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:
parent
ebc9abd4e2
commit
3e34b3c7a7
@ -31,6 +31,9 @@ imports
|
||||
# accounting archives
|
||||
accounting
|
||||
|
||||
# Proof of identity files
|
||||
proof_of_identity_files
|
||||
|
||||
# Development files
|
||||
Vagrantfile
|
||||
provision
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -46,6 +46,9 @@
|
||||
# Archives of closed accounting periods
|
||||
/accounting/*
|
||||
|
||||
# Proof of identity files
|
||||
/proof_of_identity_files/*
|
||||
|
||||
.DS_Store
|
||||
|
||||
.vagrant
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
50
app/controllers/api/profile_custom_fields_controller.rb
Normal file
50
app/controllers/api/profile_custom_fields_controller.rb
Normal 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
|
54
app/controllers/api/proof_of_identity_files_controller.rb
Normal file
54
app/controllers/api/proof_of_identity_files_controller.rb
Normal 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
|
32
app/controllers/api/proof_of_identity_refusals_controller.rb
Normal file
32
app/controllers/api/proof_of_identity_refusals_controller.rb
Normal 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
|
50
app/controllers/api/proof_of_identity_types_controller.rb
Normal file
50
app/controllers/api/proof_of_identity_types_controller.rb
Normal 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
|
@ -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]
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
30
app/frontend/src/javascript/api/profile-custom-field.ts
Normal file
30
app/frontend/src/javascript/api/profile-custom-field.ts
Normal 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;
|
||||
}
|
||||
}
|
36
app/frontend/src/javascript/api/proof-of-identity-file.ts
Normal file
36
app/frontend/src/javascript/api/proof-of-identity-file.ts
Normal 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('&');
|
||||
}
|
||||
}
|
21
app/frontend/src/javascript/api/proof-of-identity-refusal.ts
Normal file
21
app/frontend/src/javascript/api/proof-of-identity-refusal.ts
Normal 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('&');
|
||||
}
|
||||
}
|
36
app/frontend/src/javascript/api/proof-of-identity-type.ts
Normal file
36
app/frontend/src/javascript/api/proof-of-identity-type.ts
Normal 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('&');
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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']));
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
@ -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']));
|
@ -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']));
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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']));
|
@ -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']));
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -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 */
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
]);
|
||||
|
@ -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 */
|
||||
|
||||
/**
|
||||
|
@ -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';
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
]);
|
@ -0,0 +1,6 @@
|
||||
export interface ProfileCustomField {
|
||||
id: number,
|
||||
label: string,
|
||||
required: boolean,
|
||||
actived: boolean
|
||||
}
|
11
app/frontend/src/javascript/models/proof-of-identity-file.ts
Normal file
11
app/frontend/src/javascript/models/proof-of-identity-file.ts
Normal 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,
|
||||
}
|
@ -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>,
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
export interface ProofOfIdentityTypeIndexfilter {
|
||||
group_id?: number,
|
||||
}
|
||||
|
||||
export interface ProofOfIdentityType {
|
||||
id: number,
|
||||
name: string,
|
||||
group_ids: Array<number>
|
||||
}
|
@ -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;
|
||||
|
@ -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'
|
||||
|
@ -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; }],
|
||||
|
@ -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)));
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
11
app/frontend/src/javascript/services/profile_custom_field.js
Normal file
11
app/frontend/src/javascript/services/profile_custom_field.js
Normal 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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -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"
|
||||
|
@ -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">
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
107
app/frontend/templates/admin/settings/compte.html
Normal file
107
app/frontend/templates/admin/settings/compte.html
Normal 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"/>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -35,6 +35,7 @@
|
||||
operator="currentUser"
|
||||
on-error="onError"
|
||||
on-success="onSuccess"
|
||||
ng-if="isShowPacks()"
|
||||
refresh="afterPaymentPromise">
|
||||
</packs-summary>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -26,4 +26,8 @@ class CartItem::BaseItem
|
||||
end
|
||||
|
||||
def to_object; end
|
||||
|
||||
def type
|
||||
''
|
||||
end
|
||||
end
|
||||
|
@ -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
|
||||
|
@ -49,6 +49,10 @@ class CartItem::EventReservation < CartItem::Reservation
|
||||
@reservable.title
|
||||
end
|
||||
|
||||
def type
|
||||
'event'
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def tickets_params
|
||||
|
@ -35,4 +35,8 @@ class CartItem::FreeExtension < CartItem::BaseItem
|
||||
end_at: @new_expiration_date
|
||||
)
|
||||
end
|
||||
|
||||
def type
|
||||
'subscription'
|
||||
end
|
||||
end
|
||||
|
@ -21,6 +21,10 @@ class CartItem::MachineReservation < CartItem::Reservation
|
||||
)
|
||||
end
|
||||
|
||||
def type
|
||||
'machine'
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def credits
|
||||
|
@ -29,4 +29,8 @@ class CartItem::PaymentSchedule
|
||||
|
||||
{ schedule: schedule, total: total_amount }
|
||||
end
|
||||
|
||||
def type
|
||||
'subscription'
|
||||
end
|
||||
end
|
||||
|
@ -33,4 +33,8 @@ class CartItem::PrepaidPack < CartItem::BaseItem
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
)
|
||||
end
|
||||
|
||||
def type
|
||||
'pack'
|
||||
end
|
||||
end
|
||||
|
@ -21,6 +21,10 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
)
|
||||
end
|
||||
|
||||
def type
|
||||
'space'
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def credits
|
||||
|
@ -37,4 +37,8 @@ class CartItem::Subscription < CartItem::BaseItem
|
||||
start_at: @start_at
|
||||
)
|
||||
end
|
||||
|
||||
def type
|
||||
'subscription'
|
||||
end
|
||||
end
|
||||
|
@ -41,6 +41,10 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
||||
)
|
||||
end
|
||||
|
||||
def type
|
||||
'training'
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def credits
|
||||
|
@ -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') }
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
4
app/models/profile_custom_field.rb
Normal file
4
app/models/profile_custom_field.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class ProfileCustomField < ApplicationRecord
|
||||
has_many :user_profile_custom_fields
|
||||
has_many :invoicing_profiles, through: :user_profile_custom_fields
|
||||
end
|
12
app/models/proof_of_identity_file.rb
Normal file
12
app/models/proof_of_identity_file.rb
Normal 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
|
7
app/models/proof_of_identity_refusal.rb
Normal file
7
app/models/proof_of_identity_refusal.rb
Normal 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
|
8
app/models/proof_of_identity_type.rb
Normal file
8
app/models/proof_of_identity_type.rb
Normal 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
|
6
app/models/proof_of_identity_types_group.rb
Normal file
6
app/models/proof_of_identity_types_group.rb
Normal file
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProofOfIdentityTypesGroup < ApplicationRecord
|
||||
belongs_to :proof_of_identity_type
|
||||
belongs_to :group
|
||||
end
|
@ -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
Loading…
x
Reference in New Issue
Block a user