1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

add user validation required setting, user proof of identity upload and organization custom field

This commit is contained in:
Du Peng 2022-03-18 19:44:30 +01:00
parent ebc9abd4e2
commit 3e34b3c7a7
202 changed files with 3855 additions and 300 deletions

View File

@ -31,6 +31,9 @@ imports
# accounting archives
accounting
# Proof of identity files
proof_of_identity_files
# Development files
Vagrantfile
provision

3
.gitignore vendored
View File

@ -46,6 +46,9 @@
# Archives of closed accounting periods
/accounting/*
# Proof of identity files
/proof_of_identity_files/*
.DS_Store
.vagrant

View File

@ -33,6 +33,7 @@
- Fix a security issue: updated rails to 5.2.7.1 to fix [CVE-2022-22577](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-22577) and [CVE-2022-27777](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-27777)
- [TODO DEPLOY] `rails db:seed`
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
- [TODO DEPLOY] add the `MAX_PROOF_OF_IDENTITY_FILE_SIZE` environment variable (see [doc/environment.md](doc/environment.md#MAX_PROOF_OF_IDENTITY_FILE_SIZE) for configuration details)
## v5.3.13 2022 May 02

View File

@ -81,6 +81,7 @@ VOLUME /usr/src/app/public
VOLUME /usr/src/app/public/uploads
VOLUME /usr/src/app/public/packs
VOLUME /usr/src/app/accounting
VOLUME /usr/src/app/proof_of_identity_files
VOLUME /var/log/supervisor
# Expose port 3000 to the Docker host, so we can access it from the outside

View File

@ -3,7 +3,7 @@
# API Controller for resources of type User with role 'member'
class API::MembersController < API::ApiController
before_action :authenticate_user!, except: [:last_subscribed]
before_action :set_member, only: %i[update destroy merge complete_tour update_role]
before_action :set_member, only: %i[update destroy merge complete_tour update_role validate]
respond_to :json
def index
@ -240,6 +240,18 @@ class API::MembersController < API::ApiController
render json: @member
end
def validate
authorize @member
members_service = Members::MembersService.new(@member)
if members_service.validate(user_params[:validated_at].present?)
render :show, status: :ok, location: member_path(@member)
else
render json: @member.errors, status: :unprocessable_entity
end
end
private
def set_member
@ -262,7 +274,7 @@ class API::MembersController < API::ApiController
elsif current_user.admin? || current_user.manager?
params.require(:user).permit(:username, :email, :password, :password_confirmation, :is_allow_contact, :is_allow_newsletter, :group_id,
tag_ids: [],
:validated_at, tag_ids: [],
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
# API Controller for resources of type ProfileCustomField
# ProfileCustomFields are used to provide admin config user profile custom fields
class API::ProfileCustomFieldsController < API::ApiController
before_action :authenticate_user!, except: :index
before_action :set_profile_custom_field, only: %i[show update destroy]
def index
@profile_custom_fields = ProfileCustomField.all.order('id ASC')
end
def show; end
def create
authorize ProofOfIdentityType
@profile_custom_field = ProfileCustomField.new(profile_custom_field_params)
if @profile_custom_field.save
render status: :created
else
render json: @profile_custom_field.errors.full_messages, status: :unprocessable_entity
end
end
def update
authorize @profile_custom_field
if @profile_custom_field.update(profile_custom_field_params)
render status: :ok
else
render json: @pack.errors.full_messages, status: :unprocessable_entity
end
end
def destroy
authorize @profile_custom_field
@profile_custom_field.destroy
head :no_content
end
private
def set_profile_custom_field
@profile_custom_field = ProfileCustomField.find(params[:id])
end
def profile_custom_field_params
params.require(:profile_custom_field).permit(:label, :required, :actived)
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
# API Controller for resources of type ProofOfIdentityFile
# ProofOfIdentityFiles are used in settings
class API::ProofOfIdentityFilesController < API::ApiController
before_action :authenticate_user!
before_action :set_proof_of_identity_file, only: %i[show update download]
def index
@proof_of_identity_files = ProofOfIdentityFileService.list(current_user, params)
end
# PUT /api/proof_of_identity_files/1/
def update
authorize @proof_of_identity_file
if ProofOfIdentityFileService.update(@proof_of_identity_file, proof_of_identity_file_params)
render :show, status: :ok, location: @proof_of_identity_file
else
render json: @proof_of_identity_file.errors, status: :unprocessable_entity
end
end
# POST /api/proof_of_identity_files/
def create
@proof_of_identity_file = ProofOfIdentityFile.new(proof_of_identity_file_params)
authorize @proof_of_identity_file
if ProofOfIdentityFileService.create(@proof_of_identity_file)
render :show, status: :created, location: @proof_of_identity_file
else
render json: @proof_of_identity_file.errors, status: :unprocessable_entity
end
end
# GET /api/proof_of_identity_files/1/download
def download
authorize @proof_of_identity_file
send_file @proof_of_identity_file.attachment.url, type: @proof_of_identity_file.attachment.content_type, disposition: 'attachment'
end
# GET /api/proof_of_identity_files/1/
def show; end
private
def set_proof_of_identity_file
@proof_of_identity_file = ProofOfIdentityFile.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def proof_of_identity_file_params
params.required(:proof_of_identity_file).permit(:proof_of_identity_type_id, :attachment, :user_id)
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
# API Controller for resources of type ProofOfIdentityRefusal
# ProofOfIdentityRefusal are used by admin refuse user's proof of identity file
class API::ProofOfIdentityRefusalsController < API::ApiController
before_action :authenticate_user!
def index
authorize ProofOfIdentityRefusal
@proof_of_identity_files = ProofOfIdentityRefusalService.list(params)
end
def show; end
# POST /api/proof_of_identity_refusals/
def create
authorize ProofOfIdentityRefusal
@proof_of_identity_refusal = ProofOfIdentityRefusal.new(proof_of_identity_refusal_params)
if ProofOfIdentityRefusalService.create(@proof_of_identity_refusal)
render :show, status: :created, location: @proof_of_identity_refusal
else
render json: @proof_of_identity_refusal.errors, status: :unprocessable_entity
end
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def proof_of_identity_refusal_params
params.required(:proof_of_identity_refusal).permit(:message, :operator_id, :user_id, proof_of_identity_type_ids: [])
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
# API Controller for resources of type ProofOfIdentityType
# ProofOfIdentityTypes are used to provide admin config proof of identity type by group
class API::ProofOfIdentityTypesController < API::ApiController
before_action :authenticate_user!, except: :index
before_action :set_proof_of_identity_type, only: %i[show update destroy]
def index
@proof_of_identity_types = ProofOfIdentityTypeService.list(params)
end
def show; end
def create
authorize ProofOfIdentityType
@proof_of_identity_type = ProofOfIdentityType.new(proof_of_identity_type_params)
if @proof_of_identity_type.save
render status: :created
else
render json: @proof_of_identity_type.errors.full_messages, status: :unprocessable_entity
end
end
def update
authorize @proof_of_identity_type
if @proof_of_identity_type.update(proof_of_identity_type_params)
render status: :ok
else
render json: @pack.errors.full_messages, status: :unprocessable_entity
end
end
def destroy
authorize @proof_of_identity_type
@proof_of_identity_type.destroy
head :no_content
end
private
def set_proof_of_identity_type
@proof_of_identity_type = ProofOfIdentityType.find(params[:id])
end
def proof_of_identity_type_params
params.require(:proof_of_identity_type).permit(:name, group_ids: [])
end
end

View File

@ -43,6 +43,7 @@ class ApplicationController < ActionController::Base
profile_attributes: %i[phone last_name first_name interest software_mastered],
invoicing_profile_attributes: [
organization_attributes: [:name, address_attributes: [:address]],
user_profile_custom_fields_attributes: %i[profile_custom_field_id value],
address_attributes: [:address]
],
statistic_profile_attributes: %i[gender birthday]

View File

@ -39,4 +39,9 @@ export default class MemberAPI {
const res: AxiosResponse<User> = await apiClient.get('/api/members/current');
return res?.data;
}
static async validate (member: User): Promise<User> {
const res: AxiosResponse<User> = await apiClient.patch(`/api/members/${member.id}/validate`, { user: member });
return res?.data;
}
}

View File

@ -0,0 +1,30 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { ProfileCustomField } from '../models/profile-custom-field';
export default class ProfileCustomFieldAPI {
static async index (): Promise<Array<ProfileCustomField>> {
const res: AxiosResponse<Array<ProfileCustomField>> = await apiClient.get('/api/profile_custom_fields');
return res?.data;
}
static async get (id: number): Promise<ProfileCustomField> {
const res: AxiosResponse<ProfileCustomField> = await apiClient.get(`/api/profile_custom_fields/${id}`);
return res?.data;
}
static async create (profileCustomField: ProfileCustomField): Promise<ProfileCustomField> {
const res: AxiosResponse<ProfileCustomField> = await apiClient.post('/api/profile_custom_fields', { profile_custom_field: profileCustomField });
return res?.data;
}
static async update (profileCustomField: ProfileCustomField): Promise<ProfileCustomField> {
const res: AxiosResponse<ProfileCustomField> = await apiClient.patch(`/api/profile_custom_fields/${profileCustomField.id}`, { profile_custom_field: profileCustomField });
return res?.data;
}
static async destroy (profileCustomFieldId: number): Promise<void> {
const res: AxiosResponse<void> = await apiClient.delete(`/api/profile_custom_fields/${profileCustomFieldId}`);
return res?.data;
}
}

View File

@ -0,0 +1,36 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { ProofOfIdentityFile, ProofOfIdentityFileIndexFilter } from '../models/proof-of-identity-file';
export default class ProofOfIdentityFileAPI {
static async index (filters?: ProofOfIdentityFileIndexFilter): Promise<Array<ProofOfIdentityFile>> {
const res: AxiosResponse<Array<ProofOfIdentityFile>> = await apiClient.get(`/api/proof_of_identity_files${this.filtersToQuery(filters)}`);
return res?.data;
}
static async get (id: number): Promise<ProofOfIdentityFile> {
const res: AxiosResponse<ProofOfIdentityFile> = await apiClient.get(`/api/proof_of_identity_files/${id}`);
return res?.data;
}
static async create (proofOfIdentityFile: FormData): Promise<ProofOfIdentityFile> {
const res: AxiosResponse<ProofOfIdentityFile> = await apiClient.post('/api/proof_of_identity_files', proofOfIdentityFile);
return res?.data;
}
static async update (id: number, proofOfIdentityFile: FormData): Promise<ProofOfIdentityFile> {
const res: AxiosResponse<ProofOfIdentityFile> = await apiClient.patch(`/api/proof_of_identity_files/${id}`, proofOfIdentityFile);
return res?.data;
}
static async destroy (proofOfIdentityFileId: number): Promise<void> {
const res: AxiosResponse<void> = await apiClient.delete(`/api/proof_of_identity_files/${proofOfIdentityFileId}`);
return res?.data;
}
private static filtersToQuery (filters?: ProofOfIdentityFileIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -0,0 +1,21 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { ProofOfIdentityRefusal, ProofOfIdentityRefusalIndexFilter } from '../models/proof-of-identity-refusal';
export default class ProofOfIdentityRefusalAPI {
static async index (filters?: ProofOfIdentityRefusalIndexFilter): Promise<Array<ProofOfIdentityRefusal>> {
const res: AxiosResponse<Array<ProofOfIdentityRefusal>> = await apiClient.get(`/api/proof_of_identity_refusals${this.filtersToQuery(filters)}`);
return res?.data;
}
static async create (proofOfIdentityRefusal: ProofOfIdentityRefusal): Promise<ProofOfIdentityRefusal> {
const res: AxiosResponse<ProofOfIdentityRefusal> = await apiClient.post('/api/proof_of_identity_refusals', { proof_of_identity_refusal: proofOfIdentityRefusal });
return res?.data;
}
private static filtersToQuery (filters?: ProofOfIdentityRefusalIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -0,0 +1,36 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { ProofOfIdentityType, ProofOfIdentityTypeIndexfilter } from '../models/proof-of-identity-type';
export default class ProofOfIdentityTypeAPI {
static async index (filters?: ProofOfIdentityTypeIndexfilter): Promise<Array<ProofOfIdentityType>> {
const res: AxiosResponse<Array<ProofOfIdentityType>> = await apiClient.get(`/api/proof_of_identity_types${this.filtersToQuery(filters)}`);
return res?.data;
}
static async get (id: number): Promise<ProofOfIdentityType> {
const res: AxiosResponse<ProofOfIdentityType> = await apiClient.get(`/api/proof_of_identity_types/${id}`);
return res?.data;
}
static async create (proofOfIdentityType: ProofOfIdentityType): Promise<ProofOfIdentityType> {
const res: AxiosResponse<ProofOfIdentityType> = await apiClient.post('/api/proof_of_identity_types', { proof_of_identity_type: proofOfIdentityType });
return res?.data;
}
static async update (proofOfIdentityType: ProofOfIdentityType): Promise<ProofOfIdentityType> {
const res: AxiosResponse<ProofOfIdentityType> = await apiClient.patch(`/api/proof_of_identity_types/${proofOfIdentityType.id}`, { proof_of_identity_type: proofOfIdentityType });
return res?.data;
}
static async destroy (proofOfIdentityTypeId: number): Promise<void> {
const res: AxiosResponse<void> = await apiClient.delete(`/api/proof_of_identity_types/${proofOfIdentityTypeId}`);
return res?.data;
}
private static filtersToQuery (filters?: ProofOfIdentityTypeIndexfilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -14,13 +14,14 @@ interface MachineCardProps {
onEnrollRequested: (trainingId: number) => void,
onError: (message: string) => void,
onSuccess: (message: string) => void,
canProposePacks: boolean,
}
/**
* This component is a box showing the picture of the given machine and two buttons: one to start the reservation process
* and another to redirect the user to the machine description page.
*/
const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested }) => {
const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
const { t } = useTranslation('public');
// shall we display a loader to prevent double-clicking, while the machine details are loading?
@ -65,6 +66,7 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
onReserveMachine={handleReserveMachine}
onLoginRequested={onLoginRequested}
onEnrollRequested={onEnrollRequested}
canProposePacks={canProposePacks}
className="reserve-button">
<i className="fas fa-bookmark" />
{t('app.public.machine_card.book')}
@ -80,10 +82,10 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
);
};
export const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested }) => {
export const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
return (
<Loader>
<MachineCardComponent user={user} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onError={onError} onSuccess={onSuccess} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} />
<MachineCardComponent user={user} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onError={onError} onSuccess={onSuccess} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} canProposePacks={canProposePacks} />
</Loader>
);
};

View File

@ -18,12 +18,13 @@ interface MachinesListProps {
onReserveMachine: (machine: Machine) => void,
onLoginRequested: () => Promise<User>,
onEnrollRequested: (trainingId: number) => void,
canProposePacks: boolean,
}
/**
* This component shows a list of all machines and allows filtering on that list.
*/
const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user }) => {
const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user, canProposePacks }) => {
// shown machines
const [machines, setMachines] = useState<Array<Machine>>(null);
// we keep the full list of machines, for filtering
@ -68,19 +69,20 @@ const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowM
onError={onError}
onSuccess={onSuccess}
onLoginRequested={onLoginRequested}
onEnrollRequested={onEnrollRequested} />;
onEnrollRequested={onEnrollRequested}
canProposePacks={canProposePacks}/>;
})}
</div>
</div>
);
};
const MachinesListWrapper: React.FC<MachinesListProps> = ({ user, onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested }) => {
const MachinesListWrapper: React.FC<MachinesListProps> = ({ user, onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, canProposePacks }) => {
return (
<Loader>
<MachinesList user={user} onError={onError} onSuccess={onSuccess} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} />
<MachinesList user={user} onError={onError} onSuccess={onSuccess} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} canProposePacks={canProposePacks}/>
</Loader>
);
};
Application.Components.component('machinesList', react2angular(MachinesListWrapper, ['user', 'onError', 'onSuccess', 'onShowMachine', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested']));
Application.Components.component('machinesList', react2angular(MachinesListWrapper, ['user', 'onError', 'onSuccess', 'onShowMachine', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'canProposePacks']));

View File

@ -24,13 +24,14 @@ interface ReserveButtonProps {
onReserveMachine: (machine: Machine) => void,
onLoginRequested: () => Promise<User>,
onEnrollRequested: (trainingId: number) => void,
className?: string
className?: string,
canProposePacks: boolean,
}
/**
* Button component that makes the training verification before redirecting the user to the reservation calendar
*/
const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children }) => {
const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
const { t } = useTranslation('shared');
const [machine, setMachine] = useState<Machine>(null);
@ -146,7 +147,7 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
// if the customer has already bought a pack or if there's no active packs for this machine,
// or customer has not any subscription if admin active pack only for subscription option
// let the customer reserve
if (machine.current_user_has_packs || !machine.has_prepaid_packs_for_current_user || (isPackOnlyForSubscription && !user.subscribed_plan)) {
if (machine.current_user_has_packs || !machine.has_prepaid_packs_for_current_user || (isPackOnlyForSubscription && !user.subscribed_plan) || !canProposePacks) {
return onReserveMachine(machine);
}
@ -182,14 +183,14 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
);
};
export const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children }) => {
export const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
return (
<Loader>
<ReserveButtonComponent currentUser={currentUser} machineId={machineId} onError={onError} onSuccess={onSuccess} onLoadingStart={onLoadingStart} onLoadingEnd={onLoadingEnd} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} className={className}>
<ReserveButtonComponent currentUser={currentUser} machineId={machineId} onError={onError} onSuccess={onSuccess} onLoadingStart={onLoadingStart} onLoadingEnd={onLoadingEnd} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} className={className} canProposePacks={canProposePacks}>
{children}
</ReserveButtonComponent>
</Loader>
);
};
Application.Components.component('reserveButton', react2angular(ReserveButton, ['currentUser', 'machineId', 'onLoadingStart', 'onLoadingEnd', 'onError', 'onSuccess', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'className']));
Application.Components.component('reserveButton', react2angular(ReserveButton, ['currentUser', 'machineId', 'onLoadingStart', 'onLoadingEnd', 'onError', 'onSuccess', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'className', 'canProposePacks']));

View File

@ -14,6 +14,7 @@ interface PlanCardProps {
subscribedPlanId?: number,
operator: User,
isSelected: boolean,
canSelectPlan: boolean,
onSelectPlan: (plan: Plan) => void,
onLoginRequested: () => void,
}
@ -21,7 +22,7 @@ interface PlanCardProps {
/**
* This component is a "card" (visually), publicly presenting the details of a plan and allowing a user to subscribe.
*/
const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
const { t } = useTranslation('public');
/**
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
@ -88,7 +89,9 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
* Callback triggered when the user select the plan
*/
const handleSelectPlan = (): void => {
onSelectPlan(plan);
if (canSelectPlan) {
onSelectPlan(plan);
}
};
/**
* Callback triggered when a visitor (not logged-in user) select a plan
@ -141,10 +144,10 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
);
};
export const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
export const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
return (
<Loader>
<PlanCardComponent plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested}/>
<PlanCardComponent plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested} canSelectPlan={canSelectPlan}/>
</Loader>
);
};

View File

@ -22,6 +22,7 @@ interface PlansListProps {
operator?: User,
customer?: User,
subscribedPlanId?: number,
canSelectPlan: boolean,
}
// A list of plans, organized by group ID - then organized by plan-category ID (or NaN if the plan has no category)
@ -30,7 +31,7 @@ type PlansTree = Map<number, Map<number, Array<Plan>>>;
/**
* This component display an organized list of plans to allow the end-user to select one and subscribe online
*/
const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId }) => {
const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId, canSelectPlan }) => {
// all plans
const [plans, setPlans] = useState<PlansTree>(null);
// all plan-categories, ordered by weight
@ -218,6 +219,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
operator={operator}
isSelected={isSelectedPlan(plan)}
onSelectPlan={handlePlanSelection}
canSelectPlan={canSelectPlan}
onLoginRequested={onLoginRequest} />
))}
</div>
@ -239,12 +241,12 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
);
};
const PlansListWrapper: React.FC<PlansListProps> = ({ customer, onError, onPlanSelection, onLoginRequest, operator, subscribedPlanId }) => {
const PlansListWrapper: React.FC<PlansListProps> = ({ customer, onError, onPlanSelection, onLoginRequest, operator, subscribedPlanId, canSelectPlan }) => {
return (
<Loader>
<PlansList customer={customer} onError={onError} onPlanSelection={onPlanSelection} onLoginRequest={onLoginRequest} operator={operator} subscribedPlanId={subscribedPlanId} />
<PlansList customer={customer} onError={onError} onPlanSelection={onPlanSelection} onLoginRequest={onLoginRequest} operator={operator} subscribedPlanId={subscribedPlanId} canSelectPlan={canSelectPlan} />
</Loader>
);
};
Application.Components.component('plansList', react2angular(PlansListWrapper, ['customer', 'onError', 'onPlanSelection', 'onLoginRequest', 'operator', 'subscribedPlanId']));
Application.Components.component('plansList', react2angular(PlansListWrapper, ['customer', 'onError', 'onPlanSelection', 'onLoginRequest', 'operator', 'subscribedPlanId', 'canSelectPlan']));

View File

@ -0,0 +1,146 @@
import React, { useState, useEffect, BaseSyntheticEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import Switch from 'react-switch';
import _ from 'lodash';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { ProfileCustomField } from '../../models/profile-custom-field';
import ProfileCustomFieldAPI from '../../api/profile-custom-field';
declare const Application: IApplication;
interface ProfileCustomFieldsListProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a list of all profile custom fields
*/
const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [profileCustomFields, setProfileCustomFields] = useState<Array<ProfileCustomField>>([]);
const [profileCustomFieldToEdit, setProfileCustomFieldToEdit] = useState<ProfileCustomField>(null);
// get profile custom fields
useEffect(() => {
ProfileCustomFieldAPI.index().then(pData => {
setProfileCustomFields(pData);
});
}, []);
const saveProfileCustomField = (profileCustomField: ProfileCustomField) => {
ProfileCustomFieldAPI.update(profileCustomField).then(data => {
const newFields = profileCustomFields.map(f => {
if (f.id === data.id) {
return data;
}
return f;
});
setProfileCustomFields(newFields);
if (profileCustomFieldToEdit) {
setProfileCustomFieldToEdit(null);
}
onSuccess(t('app.admin.settings.compte.organization_profile_custom_field_successfully_updated'));
}).catch(err => {
onError(t('app.admin.settings.compte.organization_profile_custom_field_unable_to_update') + err);
});
};
/**
* Callback triggered when the 'switch' is changed.
*/
const handleSwitchChanged = (profileCustomField: ProfileCustomField, field: string) => {
return (value: boolean) => {
const _profileCustomField = _.clone(profileCustomField);
_profileCustomField[field] = value;
if (field === 'actived' && !value) {
_profileCustomField.required = false;
}
saveProfileCustomField(_profileCustomField);
};
};
const editProfileCustomFieldLabel = (profileCustomField: ProfileCustomField) => {
return () => {
setProfileCustomFieldToEdit(_.clone(profileCustomField));
};
};
const onChangeProfileCustomFieldLabel = (e: BaseSyntheticEvent) => {
const { value } = e.target;
setProfileCustomFieldToEdit({
...profileCustomFieldToEdit,
label: value
});
};
const saveProfileCustomFieldLabel = () => {
saveProfileCustomField(profileCustomFieldToEdit);
};
const cancelEditProfileCustomFieldLabel = () => {
setProfileCustomFieldToEdit(null);
};
return (
<table className="table profile-custom-fields-list">
<thead>
<tr>
<th style={{ width: '50%' }}></th>
<th style={{ width: '25%' }}></th>
<th style={{ width: '25%' }}></th>
</tr>
</thead>
<tbody>
{profileCustomFields.map(field => {
return (
<tr key={field.id}>
<td>
{profileCustomFieldToEdit?.id !== field.id && field.label}
{profileCustomFieldToEdit?.id !== field.id && (
<button className="btn btn-default edit-profile-custom-field-label m-r-xs pull-right" onClick={editProfileCustomFieldLabel(field)}>
<i className="fa fa-edit"></i>
</button>
)}
{profileCustomFieldToEdit?.id === field.id && (
<div>
<input className="profile-custom-field-label-input" style={{ width: '80%', height: '38px' }} type="text" value={profileCustomFieldToEdit.label} onChange={onChangeProfileCustomFieldLabel} />
<span className="buttons pull-right">
<button className="btn btn-success save-profile-custom-field-label m-r-xs" onClick={saveProfileCustomFieldLabel}>
<i className="fa fa-check"></i>
</button>
<button className="btn btn-default delete-profile-custom-field-label m-r-xs" onClick={cancelEditProfileCustomFieldLabel}>
<i className="fa fa-ban"></i>
</button>
</span>
</div>
)}
</td>
<td>
<label htmlFor="profile-custom-field-actived" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.actived')}</label>
<Switch checked={field.actived} id="profile-custom-field-actived" onChange={handleSwitchChanged(field, 'actived')} className="v-middle"></Switch>
</td>
<td>
<label htmlFor="profile-custom-field-required" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.required')}</label>
<Switch checked={field.required} disabled={!field.actived} id="profile-custom-field-required" onChange={handleSwitchChanged(field, 'required')} className="v-middle"></Switch>
</td>
</tr>
);
})}
</tbody>
</table>
);
};
const ProfileCustomFieldsListWrapper: React.FC<ProfileCustomFieldsListProps> = ({ onSuccess, onError }) => {
return (
<Loader>
<ProfileCustomFieldsList onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('profileCustomFieldsList', react2angular(ProfileCustomFieldsListWrapper, ['onSuccess', 'onError']));

View File

@ -0,0 +1,37 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
interface DeleteProofOfIdentityTypeModalProps {
isOpen: boolean,
proofOfIdentityTypeId: number,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
export const DeleteProofOfIdentityTypeModal: React.FC<DeleteProofOfIdentityTypeModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypeId, onError }) => {
const { t } = useTranslation('admin');
const handleDeleteProofOfIdentityType = async (): Promise<void> => {
try {
await ProofOfIdentityTypeAPI.destroy(proofOfIdentityTypeId);
onSuccess(t('app.admin.settings.compte.proof_of_identity_type_deleted'));
} catch (e) {
onError(t('app.admin.settings.compte.proof_of_identity_type_unable_to_delete') + e);
}
};
return (
<FabModal title={t('app.admin.settings.compte.confirmation_required')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={t('app.admin.settings.compte.confirm')}
onConfirm={handleDeleteProofOfIdentityType}
className="proof-of-identity-type-modal">
<p>{t('app.admin.settings.compte.do_you_really_want_to_delete_this_proof_of_identity_type')}</p>
</FabModal>
);
};

View File

@ -0,0 +1,169 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { HtmlTranslate } from '../base/html-translate';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityFile } from '../../models/proof-of-identity-file';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import ProofOfIdentityFileAPI from '../../api/proof-of-identity-file';
declare const Application: IApplication;
interface ProofOfIdentityFilesProps {
currentUser: User,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
interface FilesType {
number?: File
}
/**
* This component upload the proof of identity file of member
*/
const ProofOfIdentityFiles: React.FC<ProofOfIdentityFilesProps> = ({ currentUser, onSuccess, onError }) => {
const { t } = useTranslation('admin');
// list of proof of identity type
const [proofOfIdentityTypes, setProofOfIdentityTypes] = useState<Array<ProofOfIdentityType>>([]);
const [proofOfIdentityFiles, setProofOfIdentityFiles] = useState<Array<ProofOfIdentityFile>>([]);
const [files, setFiles] = useState<FilesType>({});
const [errors, setErrors] = useState<Array<number>>([]);
// get proof of identity type and files
useEffect(() => {
ProofOfIdentityTypeAPI.index({ group_id: currentUser.group_id }).then(tData => {
setProofOfIdentityTypes(tData);
});
ProofOfIdentityFileAPI.index({ user_id: currentUser.id }).then(fData => {
setProofOfIdentityFiles(fData);
});
}, []);
const getProofOfIdentityFileByType = (proofOfIdentityTypeId: number): ProofOfIdentityFile => {
return _.find<ProofOfIdentityFile>(proofOfIdentityFiles, { proof_of_identity_type_id: proofOfIdentityTypeId });
};
const hasFile = (proofOfIdentityTypeId: number): boolean => {
return files[proofOfIdentityTypeId] || getProofOfIdentityFileByType(proofOfIdentityTypeId);
};
/**
* Check if the current collection of proof of identity types is empty or not.
*/
const hasProofOfIdentityTypes = (): boolean => {
return proofOfIdentityTypes.length > 0;
};
const onFileChange = (poitId: number) => {
return (event) => {
const fileSize = event.target.files[0].size;
let _errors = errors;
// 5m max
if (fileSize > 5242880) {
_errors = errors.concat(poitId);
setErrors(_errors);
} else {
_errors = errors.filter(e => e !== poitId);
}
setErrors(_errors);
setFiles({
...files,
[poitId]: event.target.files[0]
});
};
};
const onFileUpload = async () => {
try {
for (const proofOfIdentityTypeId of Object.keys(files)) {
const formData = new FormData();
formData.append('proof_of_identity_file[user_id]', currentUser.id.toString());
formData.append('proof_of_identity_file[proof_of_identity_type_id]', proofOfIdentityTypeId);
formData.append('proof_of_identity_file[attachment]', files[proofOfIdentityTypeId]);
const proofOfIdentityFile = getProofOfIdentityFileByType(parseInt(proofOfIdentityTypeId, 10));
if (proofOfIdentityFile) {
await ProofOfIdentityFileAPI.update(proofOfIdentityFile.id, formData);
} else {
await ProofOfIdentityFileAPI.create(formData);
}
}
if (Object.keys(files).length > 0) {
ProofOfIdentityFileAPI.index({ user_id: currentUser.id }).then(fData => {
setProofOfIdentityFiles(fData);
setFiles({});
onSuccess(t('app.admin.members_edit.proof_of_identity_files_successfully_uploaded'));
});
}
} catch (e) {
onError(t('app.admin.members_edit.proof_of_identity_files_unable_to_upload') + e);
}
};
const getProofOfIdentityFileUrl = (poifId: number) => {
return `/api/proof_of_identity_files/${poifId}/download`;
};
return (
<section className="panel panel-default bg-light m-lg col-sm-12 col-md-12 col-lg-9">
<h3>{t('app.admin.members_edit.proof_of_identity_files')}</h3>
<p className="text-black font-sbold">{t('app.admin.members_edit.my_documents_info')}</p>
<div className="alert alert-warning">
<HtmlTranslate trKey="app.admin.members_edit.my_documents_alert" />
</div>
<div className="widget-content no-bg auto">
{proofOfIdentityTypes.map((poit: ProofOfIdentityType) => {
return (
<div className={`form-group ${errors.includes(poit.id) ? 'has-error' : ''}`} key={poit.id}>
<label className="control-label m-r">{poit.name}</label>
<div className="fileinput input-group">
<div className="form-control">
{hasFile(poit.id) && (
<div>
<i className="glyphicon glyphicon-file fileinput-exists"></i> <span className="fileinput-filename">{files[poit.id]?.name || getProofOfIdentityFileByType(poit.id).attachment}</span>
</div>
)}
{getProofOfIdentityFileByType(poit.id) && !files[poit.id] && (
<a href={getProofOfIdentityFileUrl(getProofOfIdentityFileByType(poit.id).id)} target="_blank" style={{ position: 'absolute', right: '10px' }} rel="noreferrer"><i className="fa fa-download text-black "></i></a>
)}
</div>
<span className="input-group-addon btn btn-default btn-file">
{!hasFile(poit.id) && (
<span className="fileinput-new">Parcourir</span>
)}
{hasFile(poit.id) && (
<span className="fileinput-exists">Modifier</span>
)}
<input type="file"
accept="application/pdf,image/jpeg,image/jpg,image/png"
onChange={onFileChange(poit.id)}
required />
</span>
</div>
{errors.includes(poit.id) && <span className="help-block">{t('app.admin.members_edit.proof_of_identity_file_size_error')}</span>}
</div>
);
})}
</div>
{hasProofOfIdentityTypes() && (
<button type="button" className="btn btn-warning m-b m-t pull-right" onClick={onFileUpload} disabled={errors.length > 0}>{t('app.admin.members_edit.save')}</button>
)}
</section>
);
};
const ProofOfIdentityFilesWrapper: React.FC<ProofOfIdentityFilesProps> = ({ currentUser, onSuccess, onError }) => {
return (
<Loader>
<ProofOfIdentityFiles currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('proofOfIdentityFiles', react2angular(ProofOfIdentityFilesWrapper, ['currentUser', 'onSuccess', 'onError']));

View File

@ -0,0 +1,75 @@
import React, { BaseSyntheticEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
interface ProofOfIdentityRefusalFormProps {
proofOfIdentityTypes: Array<ProofOfIdentityType>,
onChange: (field: string, value: string | Array<number>) => void,
}
/**
* Form to set the stripe's public and private keys
*/
export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProps> = ({ proofOfIdentityTypes, onChange }) => {
const { t } = useTranslation('admin');
const [values, setValues] = useState<Array<number>>([]);
const [message, setMessage] = useState<string>('');
/**
* Callback triggered when the name has changed.
*/
const handleMessageChange = (e: BaseSyntheticEvent): void => {
const { value } = e.target;
setMessage(value);
onChange('message', value);
};
/**
* Callback triggered when a checkbox is ticked or unticked.
* This function construct the resulting string, by adding or deleting the provided option identifier.
*/
const handleProofOfIdnentityTypesChange = (value: number) => {
return (event: BaseSyntheticEvent) => {
let newValues: Array<number>;
if (event.target.checked) {
newValues = values.concat(value);
} else {
newValues = values.filter(x => x !== value);
}
setValues(newValues);
onChange('proof_of_identity_type_ids', newValues);
};
};
/**
* Verify if the provided option is currently ticked (i.e. included in the value string)
*/
const isChecked = (value: number) => {
return values.includes(value);
};
return (
<div className="proof-of-identity-type-form">
<form name="proofOfIdentityRefusalForm">
<div>
{proofOfIdentityTypes.map(type => <div key={type.id} className="">
<label htmlFor={`checkbox-${type.id}`}>{type.name}</label>
<input id={`checkbox-${type.id}`} className="pull-right" type="checkbox" checked={isChecked(type.id)} onChange={handleProofOfIdnentityTypesChange(type.id)} />
</div>)}
</div>
<div className="proof-of-identity-refusal-comment-textarea m-t">
<label htmlFor="proof-of-identity-refusal-comment">{t('app.admin.members_edit.proof_of_identity_refusal_comment')}</label>
<textarea
id="proof-of-identity-refusal-comment"
value={message}
placeholder={t('app.admin.members_edit.proof_of_identity_refuse_input_message')}
onChange={handleMessageChange}
style={{ width: '100%' }}
rows={5}
required/>
</div>
</form>
</div>
);
};

View File

@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityRefusal } from '../../models/proof-of-identity-refusal';
import { User } from '../../models/user';
import ProofOfIdentityRefusalAPI from '../../api/proof-of-identity-refusal';
import { ProofOfIdentityRefusalForm } from './proof-of-identity-refusal-form';
interface ProofOfIdentityRefusalModalProps {
isOpen: boolean,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
proofOfIdentityTypes: Array<ProofOfIdentityType>,
operator: User,
member: User
}
export const ProofOfIdentityRefusalModal: React.FC<ProofOfIdentityRefusalModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypes, operator, member, onError }) => {
const { t } = useTranslation('admin');
const [data, setData] = useState<ProofOfIdentityRefusal>({
id: null,
operator_id: operator.id,
user_id: member.id,
proof_of_identity_type_ids: [],
message: ''
});
const handleProofOfIdentityRefusalChanged = (field: string, value: string | Array<number>) => {
setData({
...data,
[field]: value
});
};
const handleSaveProofOfIdentityRefusal = async (): Promise<void> => {
try {
await ProofOfIdentityRefusalAPI.create(data);
onSuccess(t('app.admin.members_edit.proof_of_identity_refusal_successfully_sent'));
} catch (e) {
onError(t('app.admin.members_edit.proof_of_identity_refusal_unable_to_send') + e);
}
};
const isPreventSaveProofOfIdentityRefusal = (): boolean => {
return !data.message || data.proof_of_identity_type_ids.length === 0;
};
return (
<FabModal title={t('app.admin.members_edit.proof_of_identity_refusal')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
confirmButton={t('app.admin.members_edit.confirm')}
onConfirm={handleSaveProofOfIdentityRefusal}
preventConfirm={isPreventSaveProofOfIdentityRefusal()}
className="proof-of-identity-type-modal">
<ProofOfIdentityRefusalForm proofOfIdentityTypes={proofOfIdentityTypes} onChange={handleProofOfIdentityRefusalChanged}/>
</FabModal>
);
};

View File

@ -0,0 +1,90 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Select from 'react-select';
import { FabInput } from '../base/fab-input';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
interface ProofOfIdentityTypeFormProps {
groups: Array<Group>,
proofOfIdentityType?: ProofOfIdentityType,
onChange: (field: string, value: string | Array<number>) => void,
}
/**
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
type selectOption = { value: number, label: string };
/**
* Form to set the stripe's public and private keys
*/
export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = ({ groups, proofOfIdentityType, onChange }) => {
const { t } = useTranslation('admin');
/**
* Convert all themes to the react-select format
*/
const buildOptions = (): Array<selectOption> => {
return groups.map(t => {
return { value: t.id, label: t.name };
});
};
/**
* Return the current groups(s), formatted to match the react-select format
*/
const groupsValues = (): Array<selectOption> => {
const res = [];
const groupIds = proofOfIdentityType?.group_ids || [];
if (groupIds.length > 0) {
groups.forEach(t => {
if (groupIds.indexOf(t.id) > -1) {
res.push({ value: t.id, label: t.name });
}
});
}
return res;
};
/**
* Callback triggered when the selection of group has changed.
*/
const handleGroupsChange = (selectedOptions: Array<selectOption>): void => {
onChange('group_ids', selectedOptions.map(o => o.value));
};
/**
* Callback triggered when the name has changed.
*/
const handleNameChange = (value: string): void => {
onChange('name', value);
};
return (
<div className="proof-of-identity-type-form">
<div className="proof-of-identity-type-form-info">
{t('app.admin.settings.compte.proof_of_identity_type_form_info')}
</div>
<form name="proofOfIdentityTypeForm">
<div className="proof-of-identity-type-select m-t">
<Select defaultValue={groupsValues()}
placeholder={t('app.admin.settings.compte.proof_of_identity_type_select_group')}
onChange={handleGroupsChange}
options={buildOptions()}
isMulti />
</div>
<div className="proof-of-identity-type-input m-t">
<FabInput id="proof_of_identity_type_name"
icon={<i className="fa fa-edit" />}
defaultValue={proofOfIdentityType?.name || ''}
placeholder={t('app.admin.settings.compte.proof_of_identity_type_input_name')}
onChange={handleNameChange}
debounce={200}
required/>
</div>
</form>
</div>
);
};

View File

@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import { ProofOfIdentityTypeForm } from './proof-of-identity-type-form';
interface ProofOfIdentityTypeModalProps {
isOpen: boolean,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
groups: Array<Group>,
proofOfIdentityType?: ProofOfIdentityType,
}
export const ProofOfIdentityTypeModal: React.FC<ProofOfIdentityTypeModalProps> = ({ isOpen, toggleModal, onSuccess, onError, proofOfIdentityType, groups }) => {
const { t } = useTranslation('admin');
const [data, setData] = useState<ProofOfIdentityType>({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
useEffect(() => {
setData({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
}, [proofOfIdentityType]);
const handleProofOfIdentityTypeChanged = (field: string, value: string | Array<number>) => {
setData({
...data,
[field]: value
});
};
const handleSaveProofOfIdentityType = async (): Promise<void> => {
try {
if (proofOfIdentityType?.id) {
await ProofOfIdentityTypeAPI.update(data);
onSuccess(t('app.admin.settings.compte.proof_of_identity_type_successfully_updated'));
} else {
await ProofOfIdentityTypeAPI.create(data);
onSuccess(t('app.admin.settings.compte.proof_of_identity_type_successfully_created'));
}
} catch (e) {
if (proofOfIdentityType?.id) {
onError(t('app.admin.settings.compte.proof_of_identity_type_unable_to_update') + e);
} else {
onError(t('app.admin.settings.compte.proof_of_identity_type_unable_to_create') + e);
}
}
};
const isPreventSaveProofOfIdentityType = (): boolean => {
return !data.name || data.group_ids.length === 0;
};
return (
<FabModal title={t(`app.admin.settings.compte.${proofOfIdentityType ? 'edit' : 'new'}_proof_of_identity_type`)}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
confirmButton={t(`app.admin.settings.compte.${proofOfIdentityType ? 'edit' : 'create'}`)}
onConfirm={handleSaveProofOfIdentityType}
preventConfirm={isPreventSaveProofOfIdentityType()}
className="proof-of-identity-type-modal">
<ProofOfIdentityTypeForm proofOfIdentityType={proofOfIdentityType} groups={groups} onChange={handleProofOfIdentityTypeChanged}/>
</FabModal>
);
};

View File

@ -0,0 +1,214 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { HtmlTranslate } from '../base/html-translate';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
import { ProofOfIdentityTypeModal } from './proof-of-identity-type-modal';
import { DeleteProofOfIdentityTypeModal } from './delete-proof-of-identity-type-modal';
import GroupAPI from '../../api/group';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
declare const Application: IApplication;
interface ProofOfIdentityTypesListProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
*/
const ProofOfIdentityTypesList: React.FC<ProofOfIdentityTypesListProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
// list of displayed proof of identity type
const [proofOfIdentityTypes, setProofOfIdentityTypes] = useState<Array<ProofOfIdentityType>>([]);
const [proofOfIdentityType, setProofOfIdentityType] = useState<ProofOfIdentityType>(null);
const [proofOfIdentityTypeOrder, setProofOfIdentityTypeOrder] = useState<string>(null);
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
const [groups, setGroups] = useState<Array<Group>>([]);
const [destroyModalIsOpen, setDestroyModalIsOpen] = useState<boolean>(false);
const [proofOfIdentityTypeId, setProofOfIdentityTypeId] = useState<number>(null);
// get groups
useEffect(() => {
GroupAPI.index({ disabled: false, admins: false }).then(data => {
setGroups(data);
ProofOfIdentityTypeAPI.index().then(pData => {
setProofOfIdentityTypes(pData);
});
});
}, []);
/**
* Check if the current collection of proof of identity types is empty or not.
*/
const hasProofOfIdentityTypes = (): boolean => {
return proofOfIdentityTypes.length > 0;
};
const addProofOfIdentityType = (): void => {
setProofOfIdentityType(null);
setModalIsOpen(true);
};
const editProofOfIdentityType = (poit: ProofOfIdentityType): () => void => {
return (): void => {
setProofOfIdentityType(poit);
setModalIsOpen(true);
};
};
const toggleCreateAndEditModal = (): void => {
setModalIsOpen(false);
};
const saveProofOfIdentityTypeOnSuccess = (message: string): void => {
setModalIsOpen(false);
ProofOfIdentityTypeAPI.index().then(pData => {
setProofOfIdentityTypes(orderProofOfIdentityTypes(pData, proofOfIdentityTypeOrder));
onSuccess(message);
}).catch((error) => {
onError('Unable to load proof of identity types' + error);
});
};
const destroyProofOfIdentityType = (id: number): () => void => {
return (): void => {
setProofOfIdentityTypeId(id);
setDestroyModalIsOpen(true);
};
};
const toggleDestroyModal = (): void => {
setDestroyModalIsOpen(false);
};
const destroyProofOfIdentityTypeOnSuccess = (message: string): void => {
setDestroyModalIsOpen(false);
ProofOfIdentityTypeAPI.index().then(pData => {
setProofOfIdentityTypes(pData);
setProofOfIdentityTypes(orderProofOfIdentityTypes(pData, proofOfIdentityTypeOrder));
onSuccess(message);
}).catch((error) => {
onError('Unable to load proof of identity types' + error);
});
};
const setOrderProofOfIdentityType = (orderBy: string): () => void => {
return () => {
let order = orderBy;
if (proofOfIdentityTypeOrder === orderBy) {
order = `-${orderBy}`;
}
setProofOfIdentityTypeOrder(order);
setProofOfIdentityTypes(orderProofOfIdentityTypes(proofOfIdentityTypes, order));
};
};
const orderProofOfIdentityTypes = (poits: Array<ProofOfIdentityType>, orderBy?: string): Array<ProofOfIdentityType> => {
if (!orderBy) {
return poits;
}
const order = orderBy[0] === '-' ? 'desc' : 'asc';
if (orderBy.search('group_name') !== -1) {
return _.orderBy(poits, (poit: ProofOfIdentityType) => getGroupName(poit.group_ids), order);
} else {
return _.orderBy(poits, 'name', order);
}
};
const orderClassName = (orderBy: string): string => {
if (proofOfIdentityTypeOrder) {
const order = proofOfIdentityTypeOrder[0] === '-' ? proofOfIdentityTypeOrder.substr(1) : proofOfIdentityTypeOrder;
if (order === orderBy) {
return `fa fa-arrows-v ${proofOfIdentityTypeOrder[0] === '-' ? 'fa-sort-alpha-desc' : 'fa-sort-alpha-asc'}`;
}
}
return 'fa fa-arrows-v';
};
const getGroupName = (groupIds: Array<number>): string => {
if (groupIds.length === groups.length && groupIds.length > 0) {
return t('app.admin.settings.compte.all_groups');
}
const _groups = _.filter(groups, (g: Group) => { return groupIds.includes(g.id); });
return _groups.map((g: Group) => g.name).join(', ');
};
return (
<div className="panel panel-default m-t-md">
<div className="panel-heading">
<span className="font-sbold">{t('app.admin.settings.compte.add_proof_of_identity_types')}</span>
</div>
<div className="panel-body">
<div className="row">
<p className="m-h">{t('app.admin.settings.compte.proof_of_identity_type_info')}</p>
<div className="alert alert-warning m-h-md row">
<div className="col-md-8">
<HtmlTranslate trKey="app.admin.settings.compte.proof_of_identity_type_no_group_info" />
</div>
<a href="/#!/admin/members?tabs=1" className="btn btn-warning pull-right m-t m-r-md col-md-3" style={{ color: '#000' }}>{t('app.admin.settings.compte.create_groups')}</a>
</div>
</div>
<div className="row">
<h3 className="m-l inline">{t('app.admin.settings.compte.proof_of_identity_type_title')}</h3>
<button name="button" className="btn btn-warning pull-right m-t m-r-md" onClick={addProofOfIdentityType}>{t('app.admin.settings.compte.add_proof_of_identity_type_button')}</button>
</div>
<ProofOfIdentityTypeModal isOpen={modalIsOpen} groups={groups} proofOfIdentityType={proofOfIdentityType} toggleModal={toggleCreateAndEditModal} onSuccess={saveProofOfIdentityTypeOnSuccess} onError={onError} />
<DeleteProofOfIdentityTypeModal isOpen={destroyModalIsOpen} proofOfIdentityTypeId={proofOfIdentityTypeId} toggleModal={toggleDestroyModal} onSuccess={destroyProofOfIdentityTypeOnSuccess} onError={onError}/>
<table className="table proof-of-identity-type-list">
<thead>
<tr>
<th style={{ width: '40%' }}><a onClick={setOrderProofOfIdentityType('group_name')}>{t('app.admin.settings.compte.proof_of_identity_type.group_name')} <i className={orderClassName('group_name')}></i></a></th>
<th style={{ width: '40%' }}><a onClick={setOrderProofOfIdentityType('name')}>{t('app.admin.settings.compte.proof_of_identity_type.name')} <i className={orderClassName('name')}></i></a></th>
<th style={{ width: '20%' }} className="buttons-col"></th>
</tr>
</thead>
<tbody>
{proofOfIdentityTypes.map(poit => {
return (
<tr key={poit.id}>
<td>{getGroupName(poit.group_ids)}</td>
<td>{poit.name}</td>
<td>
<div className="buttons">
<button className="btn btn-default edit-proof-of-identity-type m-r-xs" onClick={editProofOfIdentityType(poit)}>
<i className="fa fa-edit"></i>
</button>
<button className="btn btn-danger delete-proof-of-identity-type" onClick={destroyProofOfIdentityType(poit.id)}>
<i className="fa fa-trash"></i>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{!hasProofOfIdentityTypes() && (
<p className="text-center">
<HtmlTranslate trKey="app.admin.settings.compte.no_proof_of_identity_types" />
</p>
)}
</div>
</div>
);
};
const ProofOfIdentityTypesListWrapper: React.FC<ProofOfIdentityTypesListProps> = ({ onSuccess, onError }) => {
return (
<Loader>
<ProofOfIdentityTypesList onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('proofOfIdentityTypesList', react2angular(ProofOfIdentityTypesListWrapper, ['onSuccess', 'onError']));

View File

@ -0,0 +1,121 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityFile } from '../../models/proof-of-identity-file';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import ProofOfIdentityFileAPI from '../../api/proof-of-identity-file';
import { ProofOfIdentityRefusalModal } from './proof-of-identity-refusal-modal';
declare const Application: IApplication;
interface ProofOfIdentityValidationProps {
operator: User,
member: User
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a list of proof of identity file of member, admin can download and valid
**/
const ProofOfIdentityValidation: React.FC<ProofOfIdentityValidationProps> = ({ operator, member, onSuccess, onError }) => {
const { t } = useTranslation('admin');
// list of proof of identity type
const [proofOfIdentityTypes, setProofOfIdentityTypes] = useState<Array<ProofOfIdentityType>>([]);
const [proofOfIdentityFiles, setProofOfIdentityFiles] = useState<Array<ProofOfIdentityFile>>([]);
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
// get groups
useEffect(() => {
ProofOfIdentityTypeAPI.index({ group_id: member.group_id }).then(tData => {
setProofOfIdentityTypes(tData);
});
ProofOfIdentityFileAPI.index({ user_id: member.id }).then(fData => {
setProofOfIdentityFiles(fData);
});
}, []);
const getProofOfIdentityFileByType = (proofOfIdentityTypeId: number): ProofOfIdentityFile => {
return _.find<ProofOfIdentityFile>(proofOfIdentityFiles, { proof_of_identity_type_id: proofOfIdentityTypeId });
};
/**
* Check if the current collection of proof of identity types is empty or not.
*/
const hasProofOfIdentityTypes = (): boolean => {
return proofOfIdentityTypes.length > 0;
};
const getProofOfIdentityFileUrl = (poifId: number): string => {
return `/api/proof_of_identity_files/${poifId}/download`;
};
const openProofOfIdentityRefusalModal = (): void => {
setModalIsOpen(true);
};
const toggleModal = (): void => {
setModalIsOpen(false);
};
const saveProofOfIdentityRefusalOnSuccess = (message: string): void => {
setModalIsOpen(false);
onSuccess(message);
};
return (
<div>
<section className="panel panel-default bg-light m-lg col-sm-12 col-md-12 col-lg-7">
<h3>{t('app.admin.members_edit.proof_of_identity_files')}</h3>
<p className="text-black font-sbold">{t('app.admin.members_edit.find_below_the_proof_of_identity_files')}</p>
{proofOfIdentityTypes.map((poit: ProofOfIdentityType) => {
return (
<div key={poit.id} className="m-b">
<div className="m-b-xs">{poit.name}</div>
{getProofOfIdentityFileByType(poit.id) && (
<a href={getProofOfIdentityFileUrl(getProofOfIdentityFileByType(poit.id).id)} target="_blank" rel="noreferrer">
<span className="m-r">{getProofOfIdentityFileByType(poit.id).attachment}</span>
<i className="fa fa-download"></i>
</a>
)}
{!getProofOfIdentityFileByType(poit.id) && (
<div className="text-danger">{t('app.admin.members_edit.to_complete')}</div>
)}
</div>
);
})}
</section>
{hasProofOfIdentityTypes() && !member.validated_at && (
<section className="panel panel-default bg-light m-t-lg col-sm-12 col-md-12 col-lg-4">
<h3>{t('app.admin.members_edit.refuse_proof_of_identity_files')}</h3>
<p className="text-black">{t('app.admin.members_edit.refuse_proof_of_identity_files_info')}</p>
<button type="button" className="btn btn-warning m-b m-t" onClick={openProofOfIdentityRefusalModal}>{t('app.admin.members_edit.proof_of_identity_refusal')}</button>
<ProofOfIdentityRefusalModal
isOpen={modalIsOpen}
proofOfIdentityTypes={proofOfIdentityTypes}
toggleModal={toggleModal}
operator={operator}
member={member}
onError={onError}
onSuccess={saveProofOfIdentityRefusalOnSuccess}/>
</section>
)}
</div>
);
};
const ProofOfIdentityValidationWrapper: React.FC<ProofOfIdentityValidationProps> = ({ operator, member, onSuccess, onError }) => {
return (
<Loader>
<ProofOfIdentityValidation operator={operator} member={member} onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('proofOfIdentityValidation', react2angular(ProofOfIdentityValidationWrapper, ['operator', 'member', 'onSuccess', 'onError']));

View File

@ -0,0 +1,116 @@
import React, { useEffect, useState } from 'react';
import Switch from 'react-switch';
import _ from 'lodash';
import { AxiosResponse } from 'axios';
import { useTranslation } from 'react-i18next';
import { SettingName } from '../../models/setting';
import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
import SettingAPI from '../../api/setting';
import { Loader } from '../base/loader';
import { FabButton } from '../base/fab-button';
declare const Application: IApplication;
interface BooleanSettingProps {
name: SettingName,
label: string,
className?: string,
hideSave?: boolean,
onChange?: (value: string) => void,
onBeforeSave?: (message: string) => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component allows to configure boolean value for a setting.
*/
export const BooleanSetting: React.FC<BooleanSettingProps> = ({ name, label, className, hideSave, onChange, onSuccess, onError, onBeforeSave }) => {
const { t } = useTranslation('admin');
const [value, setValue] = useState<boolean>(false);
// on component load, we retrieve the current value of the list from the API
useEffect(() => {
SettingAPI.get(name)
.then(res => {
setValue(res.value === 'true');
if (_.isFunction(onChange)) {
onChange(res.value === 'true' ? 'true' : 'false');
}
})
.catch(err => onError(err));
}, []);
/**
* Save the built string to the Setting API
*/
const updateSetting = () => {
SettingAPI.update(name, value ? 'true' : 'false')
.then(() => onSuccess(t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.${name}`) })))
.catch(err => {
if (err.status === 304) return;
if (err.status === 423) {
onError(t('app.admin.settings.error_SETTING_locked', { SETTING: t(`app.admin.settings.${name}`) }));
return;
}
console.log(err);
onError(t('app.admin.settings.an_error_occurred_saving_the_setting'));
});
};
/**
* Callback triggered when the 'save' button is clicked.
* Save the built string to the Setting API
*/
const handleSave = () => {
if (_.isFunction(onBeforeSave)) {
const res = onBeforeSave({ value, name });
if (res && _.isFunction(res.then)) {
// res is a promise, wait for it before proceed
res.then((success: AxiosResponse) => {
if (success) updateSetting();
else setValue(false);
}, function () {
setValue(false);
});
} else {
if (res) updateSetting();
else setValue(false);
}
} else {
updateSetting();
}
};
/**
* Callback triggered when the 'switch' is changed.
*/
const handleChanged = (_value: boolean) => {
setValue(_value);
if (_.isFunction(onChange)) {
onChange(_value ? 'true' : 'false');
}
};
return (
<div className={`form-group ${className || ''}`}>
<label htmlFor={`setting-${name}`} className="control-label m-r">{label}</label>
<Switch checked={value} id={`setting-${name}}`} onChange={handleChanged} className="v-middle"></Switch>
{!hideSave && <FabButton className="btn btn-warning m-l" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton> }
</div>
);
};
export const BooleanSettingWrapper: React.FC<BooleanSettingProps> = ({ onChange, onSuccess, onError, label, className, name, hideSave, onBeforeSave }) => {
return (
<Loader>
<BooleanSetting label={label} name={name} onError={onError} onSuccess={onSuccess} onChange={onChange} className={className} hideSave={hideSave} onBeforeSave={onBeforeSave} />
</Loader>
);
};
Application.Components.component('booleanSetting', react2angular(BooleanSettingWrapper, ['className', 'name', 'label', 'onChange', 'onSuccess', 'onError', 'onBeforeSave']));

View File

@ -1,5 +1,6 @@
import React, { BaseSyntheticEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import _ from 'lodash';
import { SettingName } from '../../models/setting';
import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
@ -13,8 +14,11 @@ interface CheckListSettingProps {
name: SettingName,
label: string,
className?: string,
hideSave?: boolean,
defaultValue?: string,
// availableOptions must be like this [['option1', 'label 1'], ['option2', 'label 2']]
availableOptions: Array<Array<string>>,
onChange?: (value: string) => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
@ -23,7 +27,7 @@ interface CheckListSettingProps {
* This component allows to configure multiples values for a setting, like a check list.
* The result is stored as a string, composed of the checked values, e.g. 'option1,option2'
*/
const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, className, availableOptions, onSuccess, onError }) => {
export const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, hideSave, defaultValue, className, availableOptions, onChange, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [value, setValue] = useState<string>(null);
@ -31,7 +35,13 @@ const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, classN
// on component load, we retrieve the current value of the list from the API
useEffect(() => {
SettingAPI.get(name)
.then(res => setValue(res.value))
.then(res => {
const value = res.value === null && defaultValue ? defaultValue : res.value;
setValue(value);
if (_.isFunction(onChange)) {
onChange(value);
}
})
.catch(err => onError(err));
}, []);
@ -45,9 +55,16 @@ const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, classN
let newValue = value ? `${value},` : '';
newValue += option;
setValue(newValue);
if (_.isFunction(onChange)) {
onChange(newValue);
}
} else {
const regex = new RegExp(`,?${option}`, 'g');
setValue(value.replace(regex, ''));
const newValue = value.replace(regex, '');
setValue(newValue);
if (_.isFunction(onChange)) {
onChange(newValue);
}
}
};
};
@ -76,15 +93,15 @@ const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, classN
<input id={`setting-${name}-${option[0]}`} type="checkbox" checked={isChecked(option[0])} onChange={toggleCheckbox(option[0])} />
<label htmlFor={`setting-${name}-${option[0]}`}>{option[1]}</label>
</div>)}
<FabButton className="save" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton>
{!hideSave && <FabButton className="save" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton>}
</div>
);
};
const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name }) => {
export const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name, hideSave, defaultValue, onChange }) => {
return (
<Loader>
<CheckListSetting availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} />
<CheckListSetting availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} hideSave={hideSave} defaultValue={defaultValue} onChange={onChange} />
</Loader>
);
};

View File

@ -0,0 +1,112 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SettingName } from '../../models/setting';
import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
import SettingAPI from '../../api/setting';
import { Loader } from '../base/loader';
import { FabButton } from '../base/fab-button';
import { BooleanSetting } from './boolean-setting';
import { CheckListSetting } from './check-list-setting';
declare const Application: IApplication;
interface UserValidationSettingProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component allows to configure user validation required setting.
*/
const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [userValidationRequired, setUserValidationRequired] = useState<string>('false');
const userValidationRequiredListDefault = ['subscription', 'machine', 'event', 'space', 'training', 'pack'];
const [userValidationRequiredList, setUserValidationRequiredList] = useState<string>(null);
const userValidationRequiredOptions = userValidationRequiredListDefault.map(l => {
return [l, t(`app.admin.settings.compte.user_validation_required_list.${l}`)];
});
/**
* Save the built string to the Setting API
*/
const updateSetting = (name: SettingName, value: string) => {
SettingAPI.update(name, value)
.then(() => {
if (name === SettingName.UserValidationRequired) {
onSuccess(t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.compte.${name}`) }));
}
}).catch(err => {
if (err.status === 304) return;
if (err.status === 423) {
if (name === SettingName.UserValidationRequired) {
onError(t('app.admin.settings.error_SETTING_locked', { SETTING: t(`app.admin.settings.compte.${name}`) }));
}
return;
}
console.log(err);
onError(t('app.admin.settings.an_error_occurred_saving_the_setting'));
});
};
/**
* Callback triggered when the 'save' button is clicked.
*/
const handleSave = () => {
updateSetting(SettingName.UserValidationRequired, userValidationRequired);
if (userValidationRequiredList !== null) {
if (userValidationRequired === 'true') {
updateSetting(SettingName.UserValidationRequiredList, userValidationRequiredList);
} else {
updateSetting(SettingName.UserValidationRequiredList, null);
}
}
};
return (
<div className="user-validation-setting">
<BooleanSetting name={SettingName.UserValidationRequired}
label={t('app.admin.settings.compte.user_validation_required_option_label')}
hideSave={true}
onChange={setUserValidationRequired}
onSuccess={onSuccess}
onError={onError}>
</BooleanSetting>
{userValidationRequired === 'true' &&
<div>
<h4>{t('app.admin.settings.compte.user_validation_required_list_title')}</h4>
<p>
{t('app.admin.settings.compte.user_validation_required_list_info')}
</p>
<p className="alert alert-warning">
{t('app.admin.settings.compte.user_validation_required_list_other_info')}
</p>
<CheckListSetting name={SettingName.UserValidationRequiredList}
label=""
availableOptions={userValidationRequiredOptions}
defaultValue={userValidationRequiredListDefault.join(',')}
hideSave={true}
onChange={setUserValidationRequiredList}
onSuccess={onSuccess}
onError={onError}>
</CheckListSetting>
</div>
}
<FabButton className="btn btn-warning m-t" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton>
</div>
);
};
const UserValidationSettingWrapper: React.FC<UserValidationSettingProps> = ({ onSuccess, onError }) => {
return (
<Loader>
<UserValidationSetting onError={onError} onSuccess={onSuccess} />
</Loader>
);
};
Application.Components.component('userValidationSetting', react2angular(UserValidationSettingWrapper, ['onSuccess', 'onError']));

View File

@ -0,0 +1,58 @@
import React, { useState, useEffect } from 'react';
import Switch from 'react-switch';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
import MemberAPI from '../../api/member';
declare const Application: IApplication;
interface UserValidationProps {
member: User
onSuccess: (user: User, message: string) => void,
onError: (message: string) => void,
}
/**
* This component allows to configure boolean value for a setting.
*/
export const UserValidation: React.FC<UserValidationProps> = ({ member, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [value, setValue] = useState<boolean>(!!(member?.validated_at));
useEffect(() => {
setValue(!!(member?.validated_at));
}, [member]);
/**
* Callback triggered when the 'switch' is changed.
*/
const handleChanged = (_value: boolean) => {
setValue(_value);
const _member = _.clone(member);
if (_value) {
_member.validated_at = new Date();
} else {
_member.validated_at = null;
}
MemberAPI.validate(_member)
.then((user: User) => {
onSuccess(user, t(`app.admin.members_edit.${_value ? 'validate' : 'invalidate'}_member_success`));
}).catch(err => {
setValue(!_value);
onError(t(`app.admin.members_edit.${_value ? 'validate' : 'invalidate'}_member_error`) + err);
});
};
return (
<div className="user-validation">
<label htmlFor="user-validation-switch" className="control-label m-r">{t('app.admin.members_edit.validate_account')}</label>
<Switch checked={value} id="user-validation-switch" onChange={handleChanged} className="v-middle"></Switch>
</div>
);
};
Application.Components.component('userValidation', react2angular(UserValidation, ['member', 'onSuccess', 'onError']));

View File

@ -834,7 +834,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
$scope.stripeSecretKey = (res.isPresent ? STRIPE_SK_HIDDEN : '');
});
Payment.onlinePaymentStatus(function (res) {
$scope.onlinePaymentStatus = res.status;
const value = res.status.toString();
$scope.onlinePaymentStatus = value;
$scope.allSettings.online_payment_module = value;
});
}
};

View File

@ -126,8 +126,8 @@ class MembersController {
/**
* Controller used in the members/groups management page
*/
Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', '$uibModal', 'membersPromise', 'adminsPromise', 'partnersPromise', 'managersPromise', 'growl', 'Admin', 'AuthService', 'dialogs', '_t', 'Member', 'Export', 'User', 'uiTourService', 'settingsPromise',
function ($scope, $sce, $uibModal, membersPromise, adminsPromise, partnersPromise, managersPromise, growl, Admin, AuthService, dialogs, _t, Member, Export, User, uiTourService, settingsPromise) {
Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', '$uibModal', 'membersPromise', 'adminsPromise', 'partnersPromise', 'managersPromise', 'growl', 'Admin', 'AuthService', 'dialogs', '_t', 'Member', 'Export', 'User', 'uiTourService', 'settingsPromise', '$location',
function ($scope, $sce, $uibModal, membersPromise, adminsPromise, partnersPromise, managersPromise, growl, Admin, AuthService, dialogs, _t, Member, Export, User, uiTourService, settingsPromise, $location) {
/* PRIVATE STATIC CONSTANTS */
// number of users loaded each time we click on 'load more...'
@ -160,6 +160,9 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
// admins list
$scope.admins = adminsPromise.admins.filter(function (m) { return m.id !== Fablab.adminSysId; });
// is user validation required
$scope.enableUserValidationRequired = (settingsPromise.user_validation_required === 'true');
// Admins ordering/sorting. Default: not sorted
$scope.orderAdmin = null;
@ -176,7 +179,8 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
$scope.orderManager = null;
// default tab: members list
$scope.tabs = { active: 0, sub: 0 };
const defaultActiveTab = $location.search().tabs ? parseInt($location.search().tabs, 10) : 0;
$scope.tabs = { active: defaultActiveTab, sub: 0 };
/**
* Change the members ordering criterion to the one provided
@ -650,8 +654,8 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
/**
* Controller used in the member edition page
*/
Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$transition$', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', 'settingsPromise',
function ($scope, $state, $transition$, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet, settingsPromise) {
Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$transition$', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', 'settingsPromise', 'ProofOfIdentityType',
function ($scope, $state, $transition$, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet, settingsPromise, ProofOfIdentityType) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -675,6 +679,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// is the address required in _member_form?
$scope.addressRequired = (settingsPromise.address_required === 'true');
// is user validation required
$scope.enableUserValidationRequired = (settingsPromise.user_validation_required === 'true');
// the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
$scope.subscription = $scope.user.subscription;
@ -806,6 +813,18 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
$scope.subscription = newSubscription;
};
/**
* Callback triggered if validate member was successfully taken
*/
$scope.onValidateMemberSuccess = (_user, message) => {
growl.success(message);
setTimeout(() => {
$scope.user = _user;
$scope.user.statistic_profile.birthday = moment(_user.statistic_profile.birthday).toDate();
$scope.$apply();
}, 50);
};
/**
* Callback triggered in case of error
*/
@ -821,6 +840,13 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
$state.go('app.admin.members');
};
/**
* Callback triggered in case of success
*/
$scope.onSuccess = (message) => {
growl.success(message);
};
$scope.createWalletCreditModal = function (user, wallet) {
const modalInstance = $uibModal.open({
animation: true,
@ -918,6 +944,10 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
});
}
ProofOfIdentityType.query({ group_id: $scope.user.group_id }, function (proofOfIdentityTypes) {
$scope.hasProofOfIdentityTypes = proofOfIdentityTypes.length > 0;
});
// Using the MembersController
return new MembersController($scope, $state, Group, Training);
};

View File

@ -573,11 +573,15 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
$uibModal.open({
templateUrl: '/admin/pricing/sendCoupon.html',
resolve: {
coupon () { return coupon; }
coupon () { return coupon; },
enableUserValidationRequired () { return settingsPromise.user_validation_required === 'true'; }
},
size: 'md',
controller: ['$scope', '$uibModalInstance', 'Coupon', 'coupon', '_t', function ($scope, $uibModalInstance, Coupon, coupon, _t) {
// Member, receiver of the coupon
controller: ['$scope', '$uibModalInstance', 'Coupon', 'coupon', '_t', 'enableUserValidationRequired', function ($scope, $uibModalInstance, Coupon, coupon, _t, enableUserValidationRequired) {
// Global config: is the user validation required ?
$scope.enableUserValidationRequired = enableUserValidationRequired;
// Member, receiver of the coupon
$scope.ctrl =
{ member: null };

View File

@ -265,6 +265,20 @@ Application.Controllers.controller('AdminProjectsController', ['$scope', '$state
}
};
/**
* Shows a success message forwarded from a child react component
*/
$scope.onSuccess = function (message) {
growl.success(message);
};
/**
* Callback triggered by react components
*/
$scope.onError = function (message) {
growl.error(message);
};
/* PRIVATE SCOPE */
/**

View File

@ -79,9 +79,11 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
resolve: {
settingsPromise: ['Setting', function (Setting) {
return Setting.query({ names: "['phone_required', 'recaptcha_site_key', 'confirmation_required', 'address_required']" }).$promise;
}]
}],
profileCustomFieldsPromise: ['ProfileCustomField', function (ProfileCustomField) { return ProfileCustomField.query({}).$promise; }],
proofOfIdentityTypesPromise: ['ProofOfIdentityType', function (ProofOfIdentityType) { return ProofOfIdentityType.query({}).$promise; }]
},
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'settingsPromise', 'growl', '_t', function ($scope, $uibModalInstance, Group, CustomAsset, settingsPromise, growl, _t) {
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'settingsPromise', 'growl', '_t', 'profileCustomFieldsPromise', 'proofOfIdentityTypesPromise', function ($scope, $uibModalInstance, Group, CustomAsset, settingsPromise, growl, _t, profileCustomFieldsPromise, proofOfIdentityTypesPromise) {
// default parameters for the date picker in the account creation modal
$scope.datePicker = {
format: Fablab.uibDateFormat,
@ -108,6 +110,8 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
$scope.datePicker.opened = true;
};
$scope.profileCustomFields = profileCustomFieldsPromise.filter(f => f.actived);
// retrieve the groups (standard, student ...)
Group.query(function (groups) {
$scope.groups = groups;
@ -126,7 +130,23 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
is_allow_contact: true,
is_allow_newsletter: false,
// reCaptcha response, received from Google (through AJAX) and sent to server for validation
recaptcha: undefined
recaptcha: undefined,
invoicing_profile_attributes: {
user_profile_custom_fields_attributes: $scope.profileCustomFields.map(f => {
return { profile_custom_field_id: f.id };
})
}
};
$scope.hasProofOfIdentityTypes = function (groupId) {
return proofOfIdentityTypesPromise.filter(t => t.group_ids.includes(groupId)).length > 0;
};
$scope.groupName = function (groupId) {
if (!$scope.enabledGroups || groupId === undefined || groupId === null) {
return '';
}
return $scope.enabledGroups.find(g => g.id === groupId).name;
};
// Errors display

View File

@ -12,8 +12,8 @@
*/
'use strict';
Application.Controllers.controller('DashboardController', ['$scope', 'memberPromise', 'trainingsPromise', 'SocialNetworks', 'growl',
function ($scope, memberPromise, trainingsPromise, SocialNetworks, growl) {
Application.Controllers.controller('DashboardController', ['$scope', 'memberPromise', 'trainingsPromise', 'SocialNetworks', 'growl', 'proofOfIdentityTypesPromise',
function ($scope, memberPromise, trainingsPromise, SocialNetworks, growl, proofOfIdentityTypesPromise) {
// Current user's profile
$scope.user = memberPromise;
@ -23,6 +23,8 @@ Application.Controllers.controller('DashboardController', ['$scope', 'memberProm
networks: SocialNetworks
};
$scope.hasProofOfIdentityTypes = proofOfIdentityTypesPromise.length > 0;
/**
* Check if the member has used his training credits for the given credit
* @param trainingCredits array of credits used by the member
@ -60,7 +62,9 @@ Application.Controllers.controller('DashboardController', ['$scope', 'memberProm
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = () => $scope.social.networks = filterNetworks();
const initialize = () => {
$scope.social.networks = filterNetworks();
};
/**
* Filter the social networks or websites that are associated with the profile of the user provided in promise
@ -77,6 +81,27 @@ Application.Controllers.controller('DashboardController', ['$scope', 'memberProm
return networks;
};
/**
* Callback used in case of error
*/
$scope.onSuccess = function (message) {
growl.success(message);
};
/**
* Callback used in PaymentScheduleDashboard, in case of error
*/
$scope.onError = function (message) {
growl.error(message);
};
/**
* Callback triggered when the user has successfully updated his card
*/
$scope.onCardUpdateSuccess = function (message) {
growl.success(message);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}

View File

@ -160,6 +160,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
// Get the details for the current event (event's id is recovered from the current URL)
$scope.event = eventPromise;
// the application global settings
$scope.settings = settingsPromise;
// List of price categories for the events
$scope.priceCategories = priceCategoriesPromise;
@ -178,6 +181,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
// Message displayed to the end user about rules that applies to events reservations
$scope.eventExplicationsAlert = settingsPromise.event_explications_alert;
// Global config: is the user validation required ?
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
// online payments (by card)
$scope.onlinePayment = {
showModal: false,
@ -243,6 +249,17 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
return resetEventReserve();
};
$scope.isUserValidatedByType = () => {
return helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'event');
};
$scope.isShowReserveEventButton = () => {
return $scope.event.nb_free_places > 0 &&
!$scope.reserve.toReserve &&
$scope.now.isBefore($scope.event.end_date) &&
helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'event');
};
/**
* Callback to allow the user to set the details for his reservation
*/

View File

@ -1,7 +1,7 @@
'use strict';
Application.Controllers.controller('HeaderController', ['$scope', '$transitions', '$state', 'settingsPromise',
function ($scope, $transitions, $state, settingsPromise) {
Application.Controllers.controller('HeaderController', ['$scope', '$transitions', '$state', 'settingsPromise', 'ProofOfIdentityType', 'AuthService',
function ($scope, $transitions, $state, settingsPromise, ProofOfIdentityType, AuthService) {
$scope.aboutPage = ($state.current.name === 'app.public.about');
$transitions.onStart({}, function (trans) {
@ -14,5 +14,13 @@ Application.Controllers.controller('HeaderController', ['$scope', '$transitions'
$scope.registrationEnabled = function () {
return settingsPromise.public_registrations === 'true';
};
$scope.dropdownOnToggled = function (open) {
if (open) {
ProofOfIdentityType.query({ group_id: $scope.currentUser.group_id }, function (proofOfIdentityTypes) {
$scope.hasProofOfIdentityTypes = proofOfIdentityTypes.length > 0;
});
}
};
}
]);

View File

@ -98,10 +98,13 @@ class MachinesController {
/**
* Controller used in the public listing page, allowing everyone to see the list of machines
*/
Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'AuthService', 'Machine', '$uibModal', 'settingsPromise', 'Member', 'uiTourService', 'machinesPromise', 'growl',
function ($scope, $state, _t, AuthService, Machine, $uibModal, settingsPromise, Member, uiTourService, machinesPromise, growl) {
Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'AuthService', 'Machine', '$uibModal', 'settingsPromise', 'Member', 'uiTourService', 'machinesPromise', 'growl', 'helpers',
function ($scope, $state, _t, AuthService, Machine, $uibModal, settingsPromise, Member, uiTourService, machinesPromise, growl, helpers) {
/* PUBLIC SCOPE */
// the application global settings
$scope.settings = settingsPromise;
/**
* Redirect the user to the machine details page
*/
@ -145,6 +148,10 @@ Application.Controllers.controller('MachinesController', ['$scope', '$state', '_
$state.go('app.logged.machines_reserve', { id: machine.slug });
}
$scope.canProposePacks = function () {
return AuthService.isAuthorized(['admin', 'manager']) || !helpers.isUserValidationRequired($scope.settings, 'pack') || (helpers.isUserValidationRequired($scope.settings, 'pack') && helpers.isUserValidated($scope.currentUser));
};
/**
* Setup the feature-tour for the machines page. (admins only)
* This is intended as a contextual help (when pressing F1)
@ -357,8 +364,8 @@ Application.Controllers.controller('ShowMachineController', ['$scope', '$state',
* This controller workflow is pretty similar to the trainings reservation controller.
*/
Application.Controllers.controller('ReserveMachineController', ['$scope', '$transition$', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'growl',
function ($scope, $transition$, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig, Reservation, growl) {
Application.Controllers.controller('ReserveMachineController', ['$scope', '$transition$', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'growl', 'helpers', 'AuthService',
function ($scope, $transition$, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig, Reservation, growl, helpers, AuthService) {
/* PRIVATE STATIC CONSTANTS */
// Slot free to be booked
@ -436,6 +443,9 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
// Global config: message to the end user concerning the machine bookings
$scope.machineExplicationsAlert = settingsPromise.machine_explications_alert;
// Global config: is the user validation required ?
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
/**
* Change the last selected slot's appearance to looks like 'added to cart'
*/
@ -579,6 +589,10 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
}, 50);
};
$scope.canSelectPlan = function () {
return helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'subscription');
};
/**
* Check if the provided plan is currently selected
* @param plan {Object} Resource plan
@ -652,6 +666,10 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
growl.error(message);
};
$scope.isShowPacks = function () {
return !helpers.isUserValidationRequired($scope.settings, 'pack') || (helpers.isUserValidationRequired($scope.settings, 'pack') && helpers.isUserValidated($scope.ctrl.member));
};
/* PRIVATE SCOPE */
/**
@ -679,6 +697,9 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
* if it's too late).
*/
const calendarEventClickCb = function (event, jsEvent, view) {
if (!AuthService.isAuthorized(['admin', 'manager']) && (helpers.isUserValidationRequired($scope.settings, 'machine') && !helpers.isUserValidated($scope.ctrl.member))) {
return;
}
$scope.selectedEvent = event;
return $scope.selectionTime = new Date();
};

View File

@ -72,8 +72,8 @@ Application.Controllers.controller('MembersController', ['$scope', 'Member', 'me
/**
* Controller used when editing the current user's profile (in dashboard)
*/
Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'settingsPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t',
function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, settingsPromise, growl, dialogs, CSRF, memberPromise, groups, _t) {
Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'settingsPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t', 'proofOfIdentityTypesPromise', 'ProofOfIdentityType',
function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, settingsPromise, growl, dialogs, CSRF, memberPromise, groups, _t, proofOfIdentityTypesPromise, ProofOfIdentityType) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -128,6 +128,8 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
// This boolean value will tell if the current user is the system admin
$scope.isAdminSys = memberPromise.id === Fablab.adminSysId;
$scope.hasProofOfIdentityTypes = proofOfIdentityTypesPromise.length > 0;
/**
* Return the group object, identified by the ID set in $scope.userGroup
*/
@ -150,6 +152,9 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
}, 50);
$rootScope.currentUser.group_id = user.group_id;
Auth._currentUser.group_id = user.group_id;
ProofOfIdentityType.query({ group_id: user.group_id }, function (proofOfIdentityTypes) {
$scope.hasProofOfIdentityTypes = proofOfIdentityTypes.length > 0;
});
};
/**
@ -309,6 +314,7 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
if ($scope.activeProvider.providable_type !== 'DatabaseProvider') {
$scope.preventPassword = true;
}
// bind fields protection with sso fields
return angular.forEach(activeProviderPromise.mapping, map => $scope.preventField[map] = true);
};

View File

@ -35,6 +35,9 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
// the application global settings
$scope.settings = settingsPromise;
// Global config: is the user validation required ?
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
// Discount coupon to apply to the basket, if any
$scope.coupon =
{ applied: null };
@ -61,6 +64,9 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
$scope.selectPlan = function (plan) {
setTimeout(() => {
if ($scope.isAuthenticated()) {
if (!AuthService.isAuthorized(['admin', 'manager']) && (helpers.isUserValidationRequired($scope.settings, 'subscription') && !helpers.isUserValidated($scope.ctrl.member))) {
return;
}
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
$scope.planSelectionTime = new Date();
@ -72,6 +78,10 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
}, 50);
};
$scope.canSelectPlan = function () {
return helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'subscription');
};
/**
* Open the modal dialog allowing the user to log into the system
*/

View File

@ -307,8 +307,8 @@ Application.Controllers.controller('ShowSpaceController', ['$scope', '$state', '
* per slots.
*/
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transition$', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation',
function ($scope, $transition$, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig, Reservation) {
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transition$', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers', 'AuthService',
function ($scope, $transition$, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers, AuthService) {
/* PRIVATE STATIC CONSTANTS */
// Color of the selected event backgound
@ -383,6 +383,9 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
// Global config: message to the end user concerning the space reservation
$scope.spaceExplicationsAlert = settingsPromise.space_explications_alert;
// Global config: is the user validation required ?
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
/**
* Change the last selected slot's appearance to looks like 'added to cart'
*/
@ -523,6 +526,10 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
}, 50);
};
$scope.canSelectPlan = function () {
return helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'subscription');
};
/**
* Check if the provided plan is currently selected
* @param plan {Object} Resource plan
@ -617,6 +624,9 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
* @see http://fullcalendar.io/docs/mouse/eventClick/
*/
const calendarEventClickCb = function (event, jsEvent, view) {
if (!AuthService.isAuthorized(['admin', 'manager']) && (helpers.isUserValidationRequired($scope.settings, 'space') && !helpers.isUserValidated($scope.ctrl.member))) {
return;
}
$scope.selectedEvent = event;
$scope.selectionTime = new Date();
};

View File

@ -91,8 +91,8 @@ Application.Controllers.controller('ShowTrainingController', ['$scope', '$state'
* training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription).
*/
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$transition$', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation',
function ($scope, $transition$, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig, Reservation) {
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$transition$', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers',
function ($scope, $transition$, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers) {
/* PRIVATE STATIC CONSTANTS */
// Color of the selected event backgound
@ -170,6 +170,9 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
// Global config: message to the end user giving advice about the training reservation
$scope.trainingInformationMessage = settingsPromise.training_information_message;
// Global config: is the user validation required ?
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
/**
* Change the last selected slot's appearance to looks like 'added to cart'
*/
@ -313,6 +316,10 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
}, 50);
};
$scope.canSelectPlan = function () {
return helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'subscription');
};
/**
* Check if the provided plan is currently selected
* @param plan {Object} Resource plan
@ -407,6 +414,9 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
* @see http://fullcalendar.io/docs/mouse/eventClick/
*/
const calendarEventClickCb = function (event, jsEvent, view) {
if (!AuthService.isAuthorized(['admin', 'manager']) && (helpers.isUserValidationRequired($scope.settings, 'training') && !helpers.isUserValidated($scope.ctrl.member))) {
return;
}
$scope.selectedEvent = event;
if ($transition$.params().id === 'all') {
$scope.training = event.training;

View File

@ -1,7 +1,7 @@
'use strict';
Application.Controllers.controller('WalletController', ['$scope', 'walletPromise', 'transactionsPromise',
function ($scope, walletPromise, transactionsPromise) {
Application.Controllers.controller('WalletController', ['$scope', 'walletPromise', 'transactionsPromise', 'proofOfIdentityTypesPromise',
function ($scope, walletPromise, transactionsPromise, proofOfIdentityTypesPromise) {
/* PUBLIC SCOPE */
// current user wallet
@ -9,5 +9,7 @@ Application.Controllers.controller('WalletController', ['$scope', 'walletPromise
// current wallet transactions
$scope.transactions = transactionsPromise;
$scope.hasProofOfIdentityTypes = proofOfIdentityTypesPromise.length > 0;
}
]);

View File

@ -389,6 +389,11 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
}, 50);
};
$scope.isUserValidatedByType = (type) => {
return AuthService.isAuthorized(['admin', 'manager']) || (!helpers.isUserValidationRequired($scope.settings, type) || (
helpers.isUserValidationRequired($scope.settings, type) && helpers.isUserValidated($scope.user)));
};
/* PRIVATE SCOPE */
/**

View File

@ -1,104 +0,0 @@
Application.Directives.directive('booleanSetting', ['Setting', 'growl', '_t',
function (Setting, growl, _t) {
return ({
restrict: 'E',
scope: {
name: '@',
label: '@',
settings: '=',
classes: '@',
onBeforeSave: '='
},
templateUrl: '/admin/settings/boolean.html',
link ($scope, element, attributes) {
// The setting
$scope.setting = {
name: $scope.name,
value: ($scope.settings[$scope.name] === 'true')
};
// ID of the html input
$scope.id = `setting-${$scope.setting.name}`;
/**
* This will update the value when the user toggles the switch button
* @param checked {Boolean}
*/
$scope.toggleSetting = (checked) => {
setTimeout(() => {
$scope.setting.value = checked;
$scope.$apply();
}, 50);
};
/**
* This will force the component to update, and the child react component to re-render
*/
$scope.refreshComponent = () => {
$scope.$apply();
};
/**
* Callback to save the setting value to the database
* @param setting {{value:*, name:string}} note that the value will be stringified
*/
$scope.save = function (setting) {
if (typeof $scope.onBeforeSave === 'function') {
const res = $scope.onBeforeSave(setting);
if (res && _.isFunction(res.then)) {
// res is a promise, wait for it before proceed
res.then(function (success) {
if (success) saveValue(setting);
else resetValue();
}, function () {
resetValue();
});
} else {
if (res) saveValue(setting);
else resetValue();
}
} else {
saveValue(setting);
}
};
/* PRIVATE SCOPE */
/**
* Save the setting's new value in DB
* @param setting
*/
const saveValue = function (setting) {
const value = setting.value.toString();
Setting.update(
{ name: setting.name },
{ value },
function () {
growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) }));
$scope.settings[$scope.name] = value;
},
function (error) {
if (error.status === 304) return;
if (error.status === 423) {
growl.error(_t('app.admin.settings.error_SETTING_locked', { SETTING: _t(`app.admin.settings.${setting.name}`) }));
return;
}
growl.error(_t('app.admin.settings.an_error_occurred_saving_the_setting'));
console.log(error);
}
);
};
/**
* Reset the value of the setting to its original state (when the component loads)
*/
const resetValue = function () {
$scope.setting.value = $scope.settings[$scope.name] === 'true';
};
}
});
}
]);

View File

@ -0,0 +1,6 @@
export interface ProfileCustomField {
id: number,
label: string,
required: boolean,
actived: boolean
}

View File

@ -0,0 +1,11 @@
export interface ProofOfIdentityFileIndexFilter {
user_id: number,
}
export interface ProofOfIdentityFile {
id: number,
attachment: string,
user_id: number,
proof_of_identity_file_id: number,
}

View File

@ -0,0 +1,12 @@
export interface ProofOfIdentityRefusalIndexFilter {
user_id: number,
}
export interface ProofOfIdentityRefusal {
id: number,
message: string,
user_id: number,
operator_id: number,
proof_of_identity_type_ids: Array<number>,
}

View File

@ -0,0 +1,9 @@
export interface ProofOfIdentityTypeIndexfilter {
group_id?: number,
}
export interface ProofOfIdentityType {
id: number,
name: string,
group_ids: Array<number>
}

View File

@ -135,6 +135,8 @@ export enum SettingName {
SocialsFlickr = 'flickr',
MachinesModule = 'machines_module',
UserChangeGroup = 'user_change_group',
UserValidationRequired = 'user_validation_required',
UserValidationRequiredList = 'user_validation_required_list'
}
export type SettingValue = string|boolean|number;

View File

@ -78,6 +78,7 @@ export interface User {
training_credits: Array<number>,
machine_credits: Array<{ machine_id: number, hours_used: number }>,
last_sign_in_at: TDateISO
validated_at: TDateISO
}
type OrderingKey = 'last_name' | 'first_name' | 'email' | 'phone' | 'group' | 'plan' | 'id'

View File

@ -137,7 +137,8 @@ angular.module('application.router', ['ui.router'])
url: '/dashboard',
resolve: {
memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }],
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }]
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
proofOfIdentityTypesPromise: ['ProofOfIdentityType', 'currentUser', function (ProofOfIdentityType, currentUser) { return ProofOfIdentityType.query({ group_id: currentUser.group_id }).$promise; }]
}
})
.state('app.logged.dashboard.profile', {
@ -163,6 +164,15 @@ angular.module('application.router', ['ui.router'])
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }]
}
})
.state('app.logged.dashboard.proof_of_identity_files', {
url: '/proof_of_identity_files',
views: {
'main@': {
templateUrl: '/dashboard/proof_of_identity_files.html',
controller: 'DashboardController'
}
}
})
.state('app.logged.dashboard.projects', {
url: '/projects',
views: {
@ -317,7 +327,7 @@ angular.module('application.router', ['ui.router'])
},
resolve: {
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
}
})
.state('app.admin.machines_new', {
@ -357,7 +367,7 @@ angular.module('application.router', ['ui.router'])
return Setting.query({
names: "['machine_explications_alert', 'booking_window_start', 'booking_window_end', 'booking_move_enable', " +
"'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
"'online_payment_module', 'payment_gateway', 'overlapping_categories']"
"'online_payment_module', 'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list']"
}).$promise;
}]
}
@ -443,7 +453,8 @@ angular.module('application.router', ['ui.router'])
return Setting.query({
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
"'space_explications_alert', 'online_payment_module', 'payment_gateway', 'overlapping_categories']"
"'space_explications_alert', 'online_payment_module', 'payment_gateway', 'overlapping_categories', " +
"'user_validation_required', 'user_validation_required_list']"
}).$promise;
}]
}
@ -497,7 +508,7 @@ angular.module('application.router', ['ui.router'])
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
"'training_explications_alert', 'training_information_message', 'online_payment_module', " +
"'payment_gateway', 'overlapping_categories', 'user_validation_required_training']"
"'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list']"
}).$promise;
}]
}
@ -525,7 +536,8 @@ angular.module('application.router', ['ui.router'])
},
resolve: {
subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway', 'overlapping_categories']" }).$promise; }]
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
}
})
@ -555,7 +567,7 @@ angular.module('application.router', ['ui.router'])
resolve: {
eventPromise: ['Event', '$transition$', function (Event, $transition$) { return Event.get({ id: $transition$.params().id }).$promise; }],
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'event_explications_alert', 'online_payment_module']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'event_explications_alert', 'online_payment_module', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
}
})
@ -772,7 +784,7 @@ angular.module('application.router', ['ui.router'])
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
spacesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Space', plan_id: 'null' }).$promise; }],
spacesCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Space' }).$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'slot_duration']" }).$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'slot_duration', 'user_validation_required', 'user_validation_required_list']" }).$promise; }],
planCategories: ['PlanCategory', function (PlanCategory) { return PlanCategory.query().$promise; }]
}
})
@ -913,7 +925,7 @@ angular.module('application.router', ['ui.router'])
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'user_validation_required']" }).$promise; }]
}
})
.state('app.admin.members_new', {
@ -966,7 +978,7 @@ angular.module('application.router', ['ui.router'])
walletPromise: ['Wallet', '$transition$', function (Wallet, $transition$) { return Wallet.getWalletByUser({ user_id: $transition$.params().id }).$promise; }],
transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required', 'user_validation_required']" }).$promise; }]
}
})
.state('app.admin.admins_new', {
@ -1069,10 +1081,8 @@ angular.module('application.router', ['ui.router'])
"'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', 'public_registrations'," +
"'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', 'user_validation_required_machine', " +
"'user_validation_required_training', 'user_validation_required_subscription', 'user_validation_required_space'," +
"'user_validation_required_event', 'user_validation_required_pack', 'user_validation_required_list'," +
"'machines_module', 'user_change_group']"
"'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', " +
"'user_validation_required_list', 'machines_module', 'user_change_group']"
}).$promise;
}],
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],

View File

@ -1,9 +1,24 @@
'use strict';
Application.Services.factory('helpers', [function () {
Application.Services.factory('helpers', ['AuthService', function (AuthService) {
return ({
getAmountToPay (price, walletAmount) {
if (walletAmount > price) { return 0; } else { return price - walletAmount; }
},
isUserValidationRequired (settings, type) {
return settings.user_validation_required === 'true' &&
settings.user_validation_required_list &&
settings.user_validation_required_list.split(',').includes(type);
},
isUserValidated (user) {
return !!(user?.validated_at);
},
isUserValidatedByType (user, settings, type) {
return AuthService.isAuthorized(['admin', 'manager']) || (!this.isUserValidationRequired(settings, type) || (
this.isUserValidationRequired(settings, type) && this.isUserValidated(user)));
}
});
}]);

View File

@ -0,0 +1,11 @@
'use strict';
Application.Services.factory('ProfileCustomField', ['$resource', function ($resource) {
return $resource('/api/profile_custom_fields/:id',
{ id: '@id' }, {
update: {
method: 'PUT'
}
}
);
}]);

View File

@ -0,0 +1,11 @@
'use strict';
Application.Services.factory('ProofOfIdentityType', ['$resource', function ($resource) {
return $resource('/api/proof_of_identity_types/:id',
{ id: '@id' }, {
update: {
method: 'PUT'
}
}
);
}]);

View File

@ -6,12 +6,12 @@
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.invoices.payment.online_payment' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.invoices.payment.online_payment_info_html' | translate"></p>
<boolean-setting name="online_payment_module"
settings="allSettings"
label="app.admin.invoices.payment.enable_online_payment"
classes="m-l"
<boolean-setting name="'online_payment_module'"
label="'app.admin.invoices.payment.enable_online_payment' | translate"
class-name="'m-l'"
on-before-save="selectPaymentGateway"
fa-icon="fa-font">
on-success="onCardUpdateSuccess"
on-error="onError">
</boolean-setting>
<select-gateway-modal is-open="openSelectGatewayModal"
toggle-modal="toggleSelectGatewayModal"

View File

@ -11,8 +11,12 @@
<section class="heading-title">
<h1 class="inline">{{ 'app.shared.user_admin.user' | translate }} {{ user.name }}</h1>
<span class="label label-danger text-white" ng-show="user.need_completion" translate>{{ 'app.shared.user_admin.incomplete_profile' }}</span>
<div class="pull-right" style="top: 35%;position: relative;right: 10px;" ng-if="enableUserValidationRequired">
<user-validation member="user"
on-error="onError"
on-success="onValidateMemberSuccess" />
</div>
</section>
</div>
<div class="col-md-3">
@ -56,6 +60,14 @@
</uib-tab>
<uib-tab heading="{{ 'app.admin.members_edit.proof_of_identity_files' | translate }}" ng-show="hasProofOfIdentityTypes">
<proof-of-identity-validation
operator="currentUser"
member="user"
on-error="onError"
on-success="onSuccess" />
</uib-tab>
<uib-tab heading="{{ 'app.admin.members_edit.subscription' | translate }}" ng-if="$root.modules.plans">

View File

@ -37,17 +37,21 @@
<table class="table members-list">
<thead>
<tr>
<th style="width:4%" class="hidden-xs" ng-if="enableUserValidationRequired"></th>
<th style="width:15%"><a ng-click="setOrderMember('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='last_name', 'fa fa-sort-alpha-desc': member.order=='-last_name', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%"><a ng-click="setOrderMember('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%" class="hidden-xs"><a ng-click="setOrderMember('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width: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>
<th style="width:9%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:14%" class="hidden-xs hidden-sm"><a ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:14%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('plan')">{{ 'app.admin.members.subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:14%" class="buttons-col"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="m in members">
<td class="text-center" ng-if="enableUserValidationRequired">
<span ng-class="{ 'text-success': !!m.validated_at }"><i class="fa fa-user-check"></i></span>
</td>
<td class="text-c">{{ m.profile.last_name }}</td>
<td class="text-c">{{ m.profile.first_name }}</td>
<td class="hidden-xs">{{ m.email }}</td>

View File

@ -87,10 +87,11 @@
</div>
<div class="row m-t" ng-show="allSettings.openlab_app_secret">
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.projects.settings.openlab_default_info_html' | translate"></p>
<boolean-setting name="openlab_default"
settings="allSettings"
label="app.admin.projects.settings.default_to_openlab"
classes="m-l"></boolean-setting>
<boolean-setting name="'openlab_default'"
label="'app.admin.projects.settings.default_to_openlab' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
</div>
</div>

View File

@ -1,5 +0,0 @@
<div class="form-group {{classes}}">
<label for="{{id}}" class="control-label m-r" translate ng-click="refreshComponent">{{ label }}</label>
<switch checked="setting.value" id="id" on-change="toggleSetting" class-name="'v-middle'" ng-if="setting"></switch>
<button name="button" class="btn btn-warning m-l" ng-click="save(setting)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>

View File

@ -0,0 +1,107 @@
<div class="panel panel-default m-t-md">
<div class="panel-heading">
<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'"
label="'app.admin.settings.general.public_registrations_allowed' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.account_confirmation' }}</h3>
<p class="alert alert-warning m-h-md" translate>
{{ 'app.admin.settings.confirmation_required_info' }}
</p>
<div class="col-md-10 col-md-offset-1">
<boolean-setting name="'confirmation_required'"
label="'app.admin.settings.confirmation_is_required' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.compte.user_validation_required_title' }}</h3>
<p class="alert alert-warning m-h-md" translate>
{{ 'app.admin.settings.compte.user_validation_required_info' }}
<div class="col-md-10 col-md-offset-1">
<user-validation-setting on-success="onSuccess" on-error="onError" />
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.captcha' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.captcha_info_html' | translate"></p>
<div class="col-md-6">
<text-setting name="recaptcha_site_key"
settings="allSettings"
label="app.admin.settings.site_key"
fa-icon="fa-info"
placeholder="0000000000000000000000000000000000000000">
</text-setting>
</div>
<div class="col-md-6">
<text-setting name="recaptcha_secret_key"
settings="allSettings"
label="app.admin.settings.secret_key"
fa-icon="fa-key"
placeholder="0000000000000000000000000000000000000000">
</text-setting>
</div>
</div>
</div>
</div>
<div class="panel panel-default m-t-md">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'app.admin.settings.compte.customize_account_settings' }}</span>
</div>
<div class="panel-body">
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.phone' }}</h3>
<p class="alert alert-warning m-h-md" translate>
{{ 'app.admin.settings.phone_required_info' }}
</p>
<div class="col-md-10 col-md-offset-1">
<boolean-setting name="'phone_required'"
label="'app.admin.settings.phone_is_required' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.address' }}</h3>
<p class="alert alert-warning m-h-md" translate>
{{ 'app.admin.settings.address_required_info_html' }}
</p>
<div class="col-md-10 col-md-offset-1">
<boolean-setting name="'address_required'"
label="'app.admin.settings.address_is_required' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.compte.organization' }}</h3>
<p class="alert alert-warning m-h-md" translate>
{{ 'app.admin.settings.compte.organization_profile_custom_fields_info' }}
<div class="col-md-12">
<profile-custom-fields-list on-success="onSuccess" on-error="onError" />
</div>
</div>
</div>
</div>
<proof-of-identity-types-list on-success="onSuccess" on-error="onError"/>

View File

@ -512,57 +512,64 @@
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.spaces' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.spaces_info_html' | translate"></p>
<boolean-setting name="spaces_module"
settings="allSettings"
label="app.admin.settings.enable_spaces"
classes="m-l"></boolean-setting>
<boolean-setting name="'spaces_module'"
label="'app.admin.settings.enable_spaces' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.plans' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.plans_info_html' | translate"></p>
<boolean-setting name="plans_module"
settings="allSettings"
label="app.admin.settings.enable_plans"
classes="m-l"></boolean-setting>
<boolean-setting name="'plans_module'"
label="'app.admin.settings.enable_plans' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.trainings' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.trainings_info_html' | translate"></p>
<boolean-setting name="trainings_module"
settings="allSettings"
label="app.admin.settings.enable_trainings"
classes="m-l"></boolean-setting>
<boolean-setting name="'trainings_module'"
label="'app.admin.settings.enable_trainings' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.invoicing' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.invoicing_info_html' | translate"></p>
<boolean-setting name="invoicing_module"
settings="allSettings"
label="app.admin.settings.enable_invoicing"
classes="m-l"></boolean-setting>
<boolean-setting name="'invoicing_module'"
label="'app.admin.settings.enable_invoicing' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.general.wallet' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.general.wallet_info_html' | translate"></p>
<boolean-setting name="wallet_module"
settings="allSettings"
label="app.admin.settings.general.enable_wallet"
classes="m-l"></boolean-setting>
<boolean-setting name="'wallet_module'"
label="'app.admin.settings.general.enable_wallet' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.general.public_agenda' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.general.public_agenda_info_html' | translate"></p>
<boolean-setting name="public_agenda_module"
settings="allSettings"
label="app.admin.settings.general.enable_public_agenda"
classes="m-l"></boolean-setting>
<boolean-setting name="'public_agenda_module'"
label="'app.admin.settings.general.enable_public_agenda' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.general.statistics' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.general.statistics_info_html' | translate"></p>
<boolean-setting name="statistics_module"
settings="allSettings"
label="app.admin.settings.general.enable_statistics"
<boolean-setting name="'statistics_module'"
label="'app.admin.settings.general.enable_statistics' | translate"
on-success="onSuccess"
on-error="onError"
classes="m-l"></boolean-setting>
</div>
</div>

View File

@ -30,19 +30,23 @@
<ng-include src="'/admin/settings/general.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.settings.home_page' | translate }}" index="1">
<uib-tab heading="{{ 'app.admin.settings.compte.compte' | translate }}" index="1">
<ng-include src="'/admin/settings/compte.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.settings.home_page' | translate }}" index="2">
<ng-include src="'/admin/settings/home_page.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.settings.about' | translate }}" index="2" class="about-page-tab">
<uib-tab heading="{{ 'app.admin.settings.about' | translate }}" index="3" class="about-page-tab">
<ng-include src="'/admin/settings/about.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.settings.privacy.title' | translate }}" index="3" class="privacy-page-tab">
<uib-tab heading="{{ 'app.admin.settings.privacy.title' | translate }}" index="4" class="privacy-page-tab">
<ng-include src="'/admin/settings/privacy.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.settings.reservations' | translate }}" index="4" class="reservations-page-tab">
<uib-tab heading="{{ 'app.admin.settings.reservations' | translate }}" index="5" class="reservations-page-tab">
<ng-include src="'/admin/settings/reservations.html'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -39,9 +39,10 @@
<div class="row">
<div class="col-md-10 col-md-offset-1">
<boolean-setting
name="fab_analytics"
settings="allSettings"
label="app.admin.settings.fab_analytics">
name="'fab_analytics'"
label="'app.admin.settings.fab_analytics' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
<p>
<span translate>{{ 'app.admin.settings.privacy.about_analytics' }}</span>

View File

@ -45,10 +45,10 @@
<div class="row">
<h3 class="m-l m-t-lg" translate>{{ 'app.admin.settings.ability_for_the_users_to_move_their_reservations' }}</h3>
<div class="col-md-6">
<boolean-setting name="booking_move_enable"
settings="allSettings"
label="app.admin.settings.reservations_shifting"
classes="m-l">
<boolean-setting name="'booking_move_enable'"
label="'app.admin.settings.reservations_shifting' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
<div class="col-md-6" ng-show="allSettings.booking_move_enable === 'true'">
@ -65,10 +65,10 @@
<div class="row">
<h3 class="m-l m-t-lg" translate>{{ 'app.admin.settings.ability_for_the_users_to_cancel_their_reservations' }}</h3>
<div class="col-md-6">
<boolean-setting name="booking_cancel_enable"
settings="allSettings"
label="app.admin.settings.reservations_cancelling"
classes="m-l">
<boolean-setting name="'booking_cancel_enable'"
label="'app.admin.settings.reservations_cancelling' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
<div class="col-md-6" ng-show="allSettings.booking_cancel_enable === 'true'">
@ -86,10 +86,10 @@
<div class="row">
<h3 class="m-l m-t-lg" translate>{{ 'app.admin.settings.book_overlapping_slots_info' }}</h3>
<div class="col-md-6">
<boolean-setting name="book_overlapping_slots"
settings="allSettings"
label="app.admin.settings.allow_booking"
classes="m-l">
<boolean-setting name="'book_overlapping_slots'"
label="'app.admin.settings.allow_booking' | translate"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
<div class="alert alert-warning" ng-show="allSettings.book_overlapping_slots !== 'true'" translate>
{{ 'app.admin.settings.overlapping_categories_info' }}
@ -122,10 +122,11 @@
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.pack_only_for_subscription_info' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.pack_only_for_subscription_info_html' | translate"></p>
<boolean-setting name="pack_only_for_subscription"
settings="allSettings"
label="app.admin.settings.pack_only_for_subscription"
classes="m-l">
<boolean-setting name="'pack_only_for_subscription'"
label="'app.admin.settings.pack_only_for_subscription' | translate"
class-name="'m-l'"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
@ -133,10 +134,11 @@
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.extended_prices' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.extended_prices_info_html' | translate"></p>
<boolean-setting name="extended_prices_in_same_day"
settings="allSettings"
label="app.admin.settings.extended_prices_in_same_day"
classes="m-l">
<boolean-setting name="'extended_prices_in_same_day'"
label="'app.admin.settings.extended_prices_in_same_day' | translate"
class-name="'m-l'"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
</div>
@ -150,10 +152,11 @@
<div class="panel-body">
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.notification_sending_before_the_reservation_occurs' }}</h3>
<boolean-setting name="reminder_enable"
settings="allSettings"
label="app.admin.settings.reservations_reminders"
classes="m-l">
<boolean-setting name="'reminder_enable'"
label="'app.admin.settings.reservations_reminders' | translate"
class-name="'m-l'"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
<div class="row" ng-show="allSettings.reminder_enable === 'true'">
@ -178,19 +181,21 @@
<div class="row" ng-show="$root.modules.machines">
<h3 class="m-l" translate>{{ 'app.admin.settings.display_machine_reservation_user_name' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.display_name_info_html' | translate"></p>
<boolean-setting name="display_name_enable"
settings="allSettings"
label="app.admin.settings.display_name"
classes="m-l">
<boolean-setting name="'display_name_enable'"
label="'app.admin.settings.display_name' | translate"
class-name="'m-l'"
on-success="onSuccess"
on-error="onError">
</boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.events_in_the_calendar' }}</h3>
<p class="alert alert-warning m-h-md" translate>{{ 'app.admin.settings.events_in_calendar_info' }}</p>
<boolean-setting name="events_in_calendar"
settings="allSettings"
label="app.admin.settings.show_event"
classes="m-l"></boolean-setting>
<boolean-setting name="'events_in_calendar'"
label="'app.admin.settings.show_event' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="section-separator"></div>

View File

@ -12,6 +12,7 @@
<ul class="nav-page nav nav-pills text-u-c text-sm">
<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 ng-if="!isAuthorized(['admin', 'manager']) && hasProofOfIdentityTypes" ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.proof_of_identity_files" translate>{{ 'app.public.common.my_proof_of_identity_files' }}</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>

View File

@ -0,0 +1,13 @@
<div>
<section class="heading">
<div class="row no-gutter">
<ng-include src="'/dashboard/nav.html'"></ng-include>
</div>
</section>
<div class="row no-gutter">
<proof-of-identity-files current-user="currentUser" on-success="onSuccess" on-error="onError" />
</div>
</div>

View File

@ -178,7 +178,13 @@
<span ng-show="reservations.length > 0" translate>{{ 'app.public.events_show.thanks_for_coming' }}</span>
<a ui-sref="app.public.events_list" translate>{{ 'app.public.events_show.view_event_list' }}</a>
</div>
<button class="btn btn-warning-full rounded btn-block text-sm" ng-click="reserveEvent()" ng-show="event.nb_free_places > 0 && !reserve.toReserve && now.isBefore(event.end_date)">{{ 'app.public.events_show.book' | translate }}</button>
<button class="btn btn-warning-full rounded btn-block text-sm" ng-click="reserveEvent()" ng-show="isShowReserveEventButton()">{{ 'app.public.events_show.book' | translate }}</button>
<uib-alert type="danger" ng-if="ctrl.member.id && !isUserValidatedByType()">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span translate>{{ 'app.shared.cart.user_validation_required_alert' }}</span>
</p>
</uib-alert>
<coupon show="reserve.totalSeats > 0 && ctrl.member" coupon="coupon.applied" total="reserve.totalNoCoupon" user-id="{{ctrl.member.id}}"></coupon>
</div>

View File

@ -47,7 +47,8 @@
on-show-machine="showMachine"
on-reserve-machine="reserveMachine"
on-login-requested="onLoginRequest"
on-enroll-requested="onEnrollRequest">
on-enroll-requested="onEnrollRequest"
can-propose-packs="canProposePacks()">
</machines-list>
</section>

View File

@ -35,6 +35,7 @@
operator="currentUser"
on-error="onError"
on-success="onSuccess"
ng-if="isShowPacks()"
refresh="afterPaymentPromise">
</packs-summary>

View File

@ -4,6 +4,7 @@
subscribed-plan-id="ctrl.member.subscribed_plan.id"
on-error="onError"
on-plan-selection="selectPlan"
can-select-plan="canSelectPlan()"
operator="currentUser">
</plans-list>

View File

@ -28,7 +28,9 @@
on-error="onError"
on-plan-selection="selectPlan"
on-login-request="userLogin"
operator="currentUser" >
operator="currentUser"
can-select-plan="canSelectPlan()"
>
</plans-list>
</div>
@ -65,6 +67,7 @@
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
reservable-type="Subscription"
after-payment="afterPayment"></cart>

View File

@ -1,4 +1,10 @@
<div class="widget panel b-a m m-t-lg" ng-if="user && !events.modifiable && !events.moved && !paidPlan">
<uib-alert type="danger m" ng-if="user && !isUserValidatedByType(reservableType.toLowerCase())">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span translate>{{ 'app.shared.cart.user_validation_required_alert' }}</span>
</p>
</uib-alert>
<div class="widget panel b-a m m-t-lg" ng-if="user && !events.modifiable && !events.moved && !paidPlan && isUserValidatedByType(reservableType.toLowerCase())">
<div class="panel-heading b-b small">
<h3 translate>{{ 'app.shared.cart.summary' }}</h3>
</div>
@ -55,7 +61,7 @@
<coupon show="isSlotsValid() && (!modePlans || selectedPlan)" coupon="coupon.applied" total="totalNoCoupon" user-id="{{user.id}}"></coupon>
<div ng-show="$root.modules.plans">
<div ng-show="$root.modules.plans && isUserValidatedByType('subscription')">
<div ng-if="isSlotsValid() && !user.subscribed_plan" ng-show="!modePlans">
<p class="font-sbold text-base l-h-2x" translate>{{ 'app.shared.cart.to_benefit_from_attractive_prices' }}</p>
<div><button class="btn btn-warning-full rounded btn-block text-xs" ng-click="showPlans()" translate>{{ 'app.shared.cart.view_our_subscriptions' }}</button></div>

View File

@ -12,5 +12,8 @@
</ui-select-choices>
</ui-select>
{{member}}
<div class="alert alert-danger m-t" style="margin-bottom: 0 !important;" ng-if="enableUserValidationRequired && ctrl.member.id && !ctrl.member.validated_at">
<span translate>{{ 'app.shared.member_select.member_not_validated' }}</span>
</div>
</div>
</div>

View File

@ -30,7 +30,7 @@
<li class="notification-open notification-center-link" ng-if="isAuthenticated()">
<a ui-sref="app.logged.notifications"><i class="fa fa-bell fa-2x black"></i> <span class="badge" ng-class="{'bg-red': notifications.unread > 0}">{{notifications.unread}}</span></a>
</li>
<li class="dropdown user-profile-nav user-menu-dropdown" ng-if="isAuthenticated()" uib-dropdown>
<li class="dropdown user-profile-nav user-menu-dropdown" ng-if="isAuthenticated()" uib-dropdown on-toggle="dropdownOnToggled(open)">
<a class="dropdown-toggle pointer" uib-dropdown-toggle>
<span class="avatar text-center">
<fab-user-avatar ng-model="currentUser.profile_attributes.user_avatar_attributes" avatar-class="thumb-50"></fab-user-avatar>
@ -41,6 +41,7 @@
<ul uib-dropdown-menu class="animated fadeInRight">
<li><a ui-sref="app.logged.dashboard.profile" translate>{{ 'app.public.common.my_profile' }}</a></li>
<li><a ui-sref="app.logged.dashboard.settings" translate>{{ 'app.public.common.my_settings' }}</a></li>
<li ng-if="!isAuthorized(['admin', 'manager']) && hasProofOfIdentityTypes"><a ui-sref="app.logged.dashboard.proof_of_identity_files" translate>{{ 'app.public.common.my_proof_of_identity_files' }}</a></li>
<li><a ui-sref="app.logged.dashboard.projects" translate>{{ 'app.public.common.my_projects' }}</a></li>
<li><a ui-sref="app.logged.dashboard.trainings" translate>{{ 'app.public.common.my_trainings' }}</a></li>
<li><a ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.common.my_events' }}</a></li>

View File

@ -138,7 +138,7 @@
<span class="input-group-addon"><i class="fa fa-building-o"></i></span>
<input type="text"
name="organization_name"
ng-model="user.profile_attributes.organization_attributes.name"
ng-model="user.invoicing_profile_attributes.organization_attributes.name"
class="form-control"
placeholder="{{ 'app.public.common.name_of_your_organization' | translate }}"
ng-required="user.organization">
@ -154,7 +154,7 @@
<span class="input-group-addon"><i class="fa fa-map-marker"></i></span>
<input type="text"
name="organization_address"
ng-model="user.profile_attributes.organization_attributes.address_attributes.address"
ng-model="user.invoicing_profile_attributes.organization_attributes.address_attributes.address"
class="form-control"
placeholder="{{ 'app.public.common.address_of_your_organization' | translate }}"
ng-required="user.organization">
@ -164,6 +164,24 @@
</div>
</div>
<div ng-repeat="(i, profileCustomField) in profileCustomFields">
<div class="form-group required-row" ng-show="user.organization" ng-class="{'has-error': signupForm.user_profile_custom_fields{{i}}.$dirty && signupForm.user_profile_custom_fields{{i}}.$invalid}">
<div class="col-sm-12">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-building-o"></i></span>
<input type="text"
name="user_profile_custom_fields{{i}}"
ng-model="user.invoicing_profile_attributes.user_profile_custom_fields_attributes[i].value"
class="form-control"
placeholder="{{profileCustomField.label}}"
ng-required="profileCustomField.required">
</div>
<span class="exponent help-cursor" title="{{ 'app.public.common.used_for_invoicing' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.user_profile_custom_fields{{i}}.$dirty && signupForm.user_profile_custom_fields{{i}}.$error.required" translate translate-values="{FEILD: profileCustomField.label}">{{ 'app.public.common.profile_custom_field_is_required' }}</span>
</div>
</div>
</div>
<div class="form-group required-row" ng-class="{'has-error': signupForm.group_id.$dirty && signupForm.group_id.$invalid}">
<div class="col-sm-12">
<div>
@ -175,6 +193,12 @@
<span class="help-block" ng-show="signupForm.group_id.$dirty && signupForm.group_id.$error.required" translate>{{ 'app.public.common.user_s_profile_is_required' }}</span>
</div>
</div>
<div class="alert alert-warning" ng-show="hasProofOfIdentityTypes(user.group_id)">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span translate translate-values="{GROUP: groupName(user.group_id)}">{{ 'app.public.common.user_proof_of_identity_files_is_required' }}</span>
</p>
</div>
<div class="form-group required-row" ng-class="{'has-error': signupForm.birthday.$dirty && signupForm.birthday.$invalid}">
<div class="col-sm-12">

View File

@ -34,7 +34,7 @@
<div class="col-sm-12 col-md-12 col-lg-3">
<div ng-if="isAuthorized(['admin', 'manager'])">
<select-member></select-member>
<select-member settings="settings"></select-member>
</div>

View File

@ -26,4 +26,8 @@ class CartItem::BaseItem
end
def to_object; end
def type
''
end
end

View File

@ -23,4 +23,8 @@ class CartItem::Coupon
{ amount: amount, total_with_coupon: new_total, total_without_coupon: cart_total }
end
def type
'coupon'
end
end

View File

@ -49,6 +49,10 @@ class CartItem::EventReservation < CartItem::Reservation
@reservable.title
end
def type
'event'
end
protected
def tickets_params

View File

@ -35,4 +35,8 @@ class CartItem::FreeExtension < CartItem::BaseItem
end_at: @new_expiration_date
)
end
def type
'subscription'
end
end

View File

@ -21,6 +21,10 @@ class CartItem::MachineReservation < CartItem::Reservation
)
end
def type
'machine'
end
protected
def credits

View File

@ -29,4 +29,8 @@ class CartItem::PaymentSchedule
{ schedule: schedule, total: total_amount }
end
def type
'subscription'
end
end

View File

@ -33,4 +33,8 @@ class CartItem::PrepaidPack < CartItem::BaseItem
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
)
end
def type
'pack'
end
end

View File

@ -21,6 +21,10 @@ class CartItem::SpaceReservation < CartItem::Reservation
)
end
def type
'space'
end
protected
def credits

View File

@ -37,4 +37,8 @@ class CartItem::Subscription < CartItem::BaseItem
start_at: @start_at
)
end
def type
'subscription'
end
end

View File

@ -41,6 +41,10 @@ class CartItem::TrainingReservation < CartItem::Reservation
)
end
def type
'training'
end
protected
def credits

View File

@ -8,6 +8,8 @@ class Group < ApplicationRecord
has_many :trainings_pricings, dependent: :destroy
has_many :machines_prices, -> { where(priceable_type: 'Machine') }, class_name: 'Price', dependent: :destroy
has_many :spaces_prices, -> { where(priceable_type: 'Space') }, class_name: 'Price', dependent: :destroy
has_many :proof_of_identity_types_groups, dependent: :destroy
has_many :proof_of_identity_types, through: :proof_of_identity_types_groups
scope :all_except_admins, -> { where.not(slug: 'admins') }

View File

@ -20,6 +20,10 @@ class InvoicingProfile < ApplicationRecord
has_many :operated_invoices, foreign_key: :operator_profile_id, class_name: 'Invoice', dependent: :nullify
has_many :operated_payment_schedules, foreign_key: :operator_profile_id, class_name: 'PaymentSchedule', dependent: :nullify
has_many :user_profile_custom_fields
has_many :profile_custom_fields, through: :user_profile_custom_fields
accepts_nested_attributes_for :user_profile_custom_fields, allow_destroy: true
validates :address, presence: true, if: -> { Setting.get('address_required') }
def full_name

View File

@ -63,6 +63,12 @@ class NotificationType
notify_member_payment_schedule_error
notify_admin_payment_schedule_gateway_canceled
notify_member_payment_schedule_gateway_canceled
notify_admin_user_proof_of_identity_files_created
notify_admin_user_proof_of_identity_files_updated
notify_user_is_validated
notify_user_is_invalidated
notify_user_proof_of_identity_refusal
notify_admin_user_proof_of_identity_refusal
]
# deprecated:
# - notify_member_subscribed_plan_is_changed

View File

@ -0,0 +1,4 @@
class ProfileCustomField < ApplicationRecord
has_many :user_profile_custom_fields
has_many :invoicing_profiles, through: :user_profile_custom_fields
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
require 'file_size_validator'
class ProofOfIdentityFile < ApplicationRecord
mount_uploader :attachment, ProofOfIdentityFileUploader
belongs_to :proof_of_identity_type
belongs_to :user
validates :attachment, file_size: { maximum: Rails.application.secrets.max_proof_of_identity_file_size&.to_i || 5.megabytes.to_i }
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class ProofOfIdentityRefusal < ApplicationRecord
belongs_to :user
belongs_to :operator, class_name: 'User', foreign_key: :operator_id
has_and_belongs_to_many :proof_of_identity_types
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class ProofOfIdentityType < ApplicationRecord
has_many :proof_of_identity_types_groups, dependent: :destroy
has_many :groups, through: :proof_of_identity_types_groups
has_many :proof_of_identity_files, dependent: :destroy
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class ProofOfIdentityTypesGroup < ApplicationRecord
belongs_to :proof_of_identity_type
belongs_to :group
end

View File

@ -145,7 +145,9 @@ class Setting < ApplicationRecord
lastfm
flickr
machines_module
user_change_group] }
user_change_group
user_validation_required
user_validation_required_list] }
# WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName

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