From 3e34b3c7a721abfb767f7e43ccc3a20129b64d34 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Fri, 18 Mar 2022 19:44:30 +0100 Subject: [PATCH] add user validation required setting, user proof of identity upload and organization custom field --- .dockerignore | 3 + .gitignore | 3 + CHANGELOG.md | 1 + Dockerfile | 1 + app/controllers/api/members_controller.rb | 16 +- .../api/profile_custom_fields_controller.rb | 50 ++++ .../api/proof_of_identity_files_controller.rb | 54 +++++ .../proof_of_identity_refusals_controller.rb | 32 +++ .../api/proof_of_identity_types_controller.rb | 50 ++++ app/controllers/application_controller.rb | 1 + app/frontend/src/javascript/api/member.ts | 5 + .../javascript/api/profile-custom-field.ts | 30 +++ .../javascript/api/proof-of-identity-file.ts | 36 +++ .../api/proof-of-identity-refusal.ts | 21 ++ .../javascript/api/proof-of-identity-type.ts | 36 +++ .../components/machines/machine-card.tsx | 8 +- .../components/machines/machines-list.tsx | 12 +- .../components/machines/reserve-button.tsx | 13 +- .../javascript/components/plans/plan-card.tsx | 11 +- .../components/plans/plans-list.tsx | 10 +- .../profile-custom-fields-list.tsx | 146 ++++++++++++ .../delete-proof-of-identity-type-modal.tsx | 37 +++ .../proof-of-identity-files.tsx | 169 ++++++++++++++ .../proof-of-identity-refusal-form.tsx | 75 ++++++ .../proof-of-identity-refusal-modal.tsx | 63 ++++++ .../proof-of-identity-type-form.tsx | 90 ++++++++ .../proof-of-identity-type-modal.tsx | 68 ++++++ .../proof-of-identity-types-list.tsx | 214 ++++++++++++++++++ .../proof-of-identity-validation.tsx | 121 ++++++++++ .../components/settings/boolean-setting.tsx | 116 ++++++++++ .../settings/check-list-setting.tsx | 29 ++- .../settings/user-validation-setting.tsx | 112 +++++++++ .../components/user/user-validation.tsx | 58 +++++ .../javascript/controllers/admin/invoices.js | 4 +- .../javascript/controllers/admin/members.js | 40 +++- .../javascript/controllers/admin/pricing.js | 10 +- .../javascript/controllers/admin/projects.js | 14 ++ .../src/javascript/controllers/application.js | 26 ++- .../src/javascript/controllers/dashboard.js | 31 ++- .../src/javascript/controllers/events.js.erb | 17 ++ .../src/javascript/controllers/header.js | 12 +- .../javascript/controllers/machines.js.erb | 29 ++- .../src/javascript/controllers/members.js | 10 +- .../src/javascript/controllers/plans.js | 10 + .../src/javascript/controllers/spaces.js.erb | 14 +- .../javascript/controllers/trainings.js.erb | 14 +- .../src/javascript/controllers/wallet.js | 6 +- .../src/javascript/directives/cart.js | 5 + .../directives/settings/boolean-setting.js | 104 --------- .../javascript/models/profile-custom-field.ts | 6 + .../models/proof-of-identity-file.ts | 11 + .../models/proof-of-identity-refusal.ts | 12 + .../models/proof-of-identity-type.ts | 9 + app/frontend/src/javascript/models/setting.ts | 2 + app/frontend/src/javascript/models/user.ts | 1 + app/frontend/src/javascript/router.js | 38 ++-- .../src/javascript/services/helpers.js | 17 +- .../services/profile_custom_field.js | 11 + .../services/proof_of_identity_type.js | 11 + .../templates/admin/invoices/payment.html | 10 +- .../templates/admin/members/edit.html | 14 +- .../templates/admin/members/members.html | 12 +- .../templates/admin/projects/settings.html | 9 +- .../templates/admin/settings/boolean.html | 5 - .../templates/admin/settings/compte.html | 107 +++++++++ .../templates/admin/settings/general.html | 61 ++--- .../templates/admin/settings/index.html | 12 +- .../templates/admin/settings/privacy.html | 7 +- .../admin/settings/reservations.html | 69 +++--- app/frontend/templates/dashboard/nav.html | 1 + .../dashboard/proof_of_identity_files.html | 13 ++ app/frontend/templates/events/show.html | 8 +- app/frontend/templates/machines/index.html | 3 +- app/frontend/templates/machines/reserve.html | 1 + app/frontend/templates/plans/_plan.html | 1 + app/frontend/templates/plans/index.html | 5 +- app/frontend/templates/shared/_cart.html | 10 +- .../templates/shared/_member_select.html | 3 + app/frontend/templates/shared/header.html.erb | 3 +- .../templates/shared/signupModal.html | 28 ++- app/frontend/templates/trainings/reserve.html | 2 +- app/models/cart_item/base_item.rb | 4 + app/models/cart_item/coupon.rb | 4 + app/models/cart_item/event_reservation.rb | 4 + app/models/cart_item/free_extension.rb | 4 + app/models/cart_item/machine_reservation.rb | 4 + app/models/cart_item/payment_schedule.rb | 4 + app/models/cart_item/prepaid_pack.rb | 4 + app/models/cart_item/space_reservation.rb | 4 + app/models/cart_item/subscription.rb | 4 + app/models/cart_item/training_reservation.rb | 4 + app/models/group.rb | 2 + app/models/invoicing_profile.rb | 4 + app/models/notification_type.rb | 6 + app/models/profile_custom_field.rb | 4 + app/models/proof_of_identity_file.rb | 12 + app/models/proof_of_identity_refusal.rb | 7 + app/models/proof_of_identity_type.rb | 8 + app/models/proof_of_identity_types_group.rb | 6 + app/models/setting.rb | 4 +- app/models/shopping_cart.rb | 17 ++ app/models/user.rb | 34 +-- app/models/user_profile_custom_field.rb | 4 + app/pdfs/pdf/invoice.rb | 11 +- app/policies/profile_custom_field_policy.rb | 16 ++ app/policies/proof_of_identity_file_policy.rb | 18 ++ .../proof_of_identity_refusal_policy.rb | 16 ++ app/policies/proof_of_identity_type_policy.rb | 16 ++ app/policies/setting_policy.rb | 3 +- app/policies/user_policy.rb | 2 +- app/services/members/members_service.rb | 32 +++ .../proof_of_identity_file_service.rb | 55 +++++ .../proof_of_identity_refusal_service.rb | 26 +++ .../proof_of_identity_type_service.rb | 15 ++ .../proof_of_identity_file_uploader.rb | 66 ++++++ app/views/api/members/_member.json.jbuilder | 2 + app/views/api/members/list.json.jbuilder | 1 + app/views/api/members/search.json.jbuilder | 3 +- ...of_of_identity_files_created.json.jbuilder | 3 + ...of_of_identity_files_updated.json.jbuilder | 3 + ...er_proof_of_identity_refusal.json.jbuilder | 3 + .../_notify_user_is_invalidated.json.jbuilder | 2 + .../_notify_user_is_validated.json.jbuilder | 2 + ...er_proof_of_identity_refusal.json.jbuilder | 2 + .../_profile_custom_field.json.jbuilder | 3 + .../create.json.jbuilder | 3 + .../profile_custom_fields/index.json.jbuilder | 5 + .../profile_custom_fields/show.json.jbuilder | 3 + .../update.json.jbuilder | 3 + .../_proof_of_identity_file.json.jbuilder | 4 + .../create.json.jbuilder | 3 + .../index.json.jbuilder | 5 + .../show.json.jbuilder | 3 + .../update.json.jbuilder | 3 + .../_proof_of_identity_refusal.json.jbuilder | 3 + .../create.json.jbuilder | 3 + .../index.json.jbuilder | 5 + .../show.json.jbuilder | 3 + .../_proof_of_identity_type.json.jbuilder | 3 + .../create.json.jbuilder | 3 + .../index.json.jbuilder | 5 + .../show.json.jbuilder | 3 + .../update.json.jbuilder | 3 + .../notify_admin_user_group_changed.html.erb | 3 + ...r_proof_of_identity_files_created.html.erb | 18 ++ ...r_proof_of_identity_files_updated.html.erb | 14 ++ ...in_user_proof_of_identity_refusal.html.erb | 15 ++ .../notify_user_is_invalidated.html.erb | 5 + .../notify_user_is_validated.html.erb | 5 + ...fy_user_proof_of_identity_refusal.html.erb | 16 ++ .../notify_user_user_group_changed.html.erb | 3 + config/locales/app.admin.de.yml | 75 ++++++ config/locales/app.admin.en.yml | 75 ++++++ config/locales/app.admin.es.yml | 75 ++++++ config/locales/app.admin.fr.yml | 75 ++++++ config/locales/app.admin.no.yml | 75 ++++++ config/locales/app.admin.pt.yml | 75 ++++++ config/locales/app.admin.zu.yml | 75 ++++++ config/locales/app.public.de.yml | 3 + config/locales/app.public.en.yml | 3 + config/locales/app.public.es.yml | 3 + config/locales/app.public.fr.yml | 3 + config/locales/app.public.no.yml | 3 + config/locales/app.public.pt.yml | 3 + config/locales/app.public.zu.yml | 3 + config/locales/app.shared.de.yml | 2 + config/locales/app.shared.en.yml | 2 + config/locales/app.shared.es.yml | 2 + config/locales/app.shared.fr.yml | 2 + config/locales/app.shared.no.yml | 1 + config/locales/app.shared.pt.yml | 2 + config/locales/app.shared.zu.yml | 2 + config/locales/de.yml | 12 + config/locales/en.yml | 12 + config/locales/es.yml | 12 + config/locales/fr.yml | 12 + config/locales/mails.de.yml | 29 +++ config/locales/mails.en.yml | 29 +++ config/locales/mails.es.yml | 29 +++ config/locales/mails.fr.yml | 29 +++ config/locales/mails.no.yml | 29 +++ config/locales/mails.pt.yml | 29 +++ config/locales/mails.zu.yml | 29 +++ config/locales/no.yml | 12 + config/locales/pt.yml | 12 + config/locales/zu.yml | 12 + config/routes.rb | 9 + config/secrets.yml | 4 + ...22090245_create_proof_of_identity_types.rb | 9 + ...9_create_proof_of_identity_types_groups.rb | 10 + ...26162334_create_proof_of_identity_files.rb | 13 ++ ...23828_create_proof_of_identity_refusals.rb | 11 + ...identity_type_proof_of_identity_refusal.rb | 8 + ...20220429164234_add_validated_at_to_user.rb | 5 + ...0506143526_create_profile_custom_fields.rb | 11 + ...05714_create_user_profile_custom_fields.rb | 11 + db/schema.rb | 66 +++++- db/seeds.rb | 9 + doc/environment.md | 8 +- env.example | 2 + setup/env.example | 2 + setup/setup.sh | 2 +- 202 files changed, 3855 insertions(+), 300 deletions(-) create mode 100644 app/controllers/api/profile_custom_fields_controller.rb create mode 100644 app/controllers/api/proof_of_identity_files_controller.rb create mode 100644 app/controllers/api/proof_of_identity_refusals_controller.rb create mode 100644 app/controllers/api/proof_of_identity_types_controller.rb create mode 100644 app/frontend/src/javascript/api/profile-custom-field.ts create mode 100644 app/frontend/src/javascript/api/proof-of-identity-file.ts create mode 100644 app/frontend/src/javascript/api/proof-of-identity-refusal.ts create mode 100644 app/frontend/src/javascript/api/proof-of-identity-type.ts create mode 100644 app/frontend/src/javascript/components/profile-custom-fields/profile-custom-fields-list.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/delete-proof-of-identity-type-modal.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-files.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-refusal-form.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-refusal-modal.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-type-form.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-type-modal.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-types-list.tsx create mode 100644 app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-validation.tsx create mode 100644 app/frontend/src/javascript/components/settings/boolean-setting.tsx create mode 100644 app/frontend/src/javascript/components/settings/user-validation-setting.tsx create mode 100644 app/frontend/src/javascript/components/user/user-validation.tsx delete mode 100644 app/frontend/src/javascript/directives/settings/boolean-setting.js create mode 100644 app/frontend/src/javascript/models/profile-custom-field.ts create mode 100644 app/frontend/src/javascript/models/proof-of-identity-file.ts create mode 100644 app/frontend/src/javascript/models/proof-of-identity-refusal.ts create mode 100644 app/frontend/src/javascript/models/proof-of-identity-type.ts create mode 100644 app/frontend/src/javascript/services/profile_custom_field.js create mode 100644 app/frontend/src/javascript/services/proof_of_identity_type.js delete mode 100644 app/frontend/templates/admin/settings/boolean.html create mode 100644 app/frontend/templates/admin/settings/compte.html create mode 100644 app/frontend/templates/dashboard/proof_of_identity_files.html create mode 100644 app/models/profile_custom_field.rb create mode 100644 app/models/proof_of_identity_file.rb create mode 100644 app/models/proof_of_identity_refusal.rb create mode 100644 app/models/proof_of_identity_type.rb create mode 100644 app/models/proof_of_identity_types_group.rb create mode 100644 app/models/user_profile_custom_field.rb create mode 100644 app/policies/profile_custom_field_policy.rb create mode 100644 app/policies/proof_of_identity_file_policy.rb create mode 100644 app/policies/proof_of_identity_refusal_policy.rb create mode 100644 app/policies/proof_of_identity_type_policy.rb create mode 100644 app/services/proof_of_identity_file_service.rb create mode 100644 app/services/proof_of_identity_refusal_service.rb create mode 100644 app/services/proof_of_identity_type_service.rb create mode 100644 app/uploaders/proof_of_identity_file_uploader.rb create mode 100644 app/views/api/notifications/_notify_admin_user_proof_of_identity_files_created.json.jbuilder create mode 100644 app/views/api/notifications/_notify_admin_user_proof_of_identity_files_updated.json.jbuilder create mode 100644 app/views/api/notifications/_notify_admin_user_proof_of_identity_refusal.json.jbuilder create mode 100644 app/views/api/notifications/_notify_user_is_invalidated.json.jbuilder create mode 100644 app/views/api/notifications/_notify_user_is_validated.json.jbuilder create mode 100644 app/views/api/notifications/_notify_user_proof_of_identity_refusal.json.jbuilder create mode 100644 app/views/api/profile_custom_fields/_profile_custom_field.json.jbuilder create mode 100644 app/views/api/profile_custom_fields/create.json.jbuilder create mode 100644 app/views/api/profile_custom_fields/index.json.jbuilder create mode 100644 app/views/api/profile_custom_fields/show.json.jbuilder create mode 100644 app/views/api/profile_custom_fields/update.json.jbuilder create mode 100644 app/views/api/proof_of_identity_files/_proof_of_identity_file.json.jbuilder create mode 100644 app/views/api/proof_of_identity_files/create.json.jbuilder create mode 100644 app/views/api/proof_of_identity_files/index.json.jbuilder create mode 100644 app/views/api/proof_of_identity_files/show.json.jbuilder create mode 100644 app/views/api/proof_of_identity_files/update.json.jbuilder create mode 100644 app/views/api/proof_of_identity_refusals/_proof_of_identity_refusal.json.jbuilder create mode 100644 app/views/api/proof_of_identity_refusals/create.json.jbuilder create mode 100644 app/views/api/proof_of_identity_refusals/index.json.jbuilder create mode 100644 app/views/api/proof_of_identity_refusals/show.json.jbuilder create mode 100644 app/views/api/proof_of_identity_types/_proof_of_identity_type.json.jbuilder create mode 100644 app/views/api/proof_of_identity_types/create.json.jbuilder create mode 100644 app/views/api/proof_of_identity_types/index.json.jbuilder create mode 100644 app/views/api/proof_of_identity_types/show.json.jbuilder create mode 100644 app/views/api/proof_of_identity_types/update.json.jbuilder create mode 100644 app/views/notifications_mailer/notify_admin_user_proof_of_identity_files_created.html.erb create mode 100644 app/views/notifications_mailer/notify_admin_user_proof_of_identity_files_updated.html.erb create mode 100644 app/views/notifications_mailer/notify_admin_user_proof_of_identity_refusal.html.erb create mode 100644 app/views/notifications_mailer/notify_user_is_invalidated.html.erb create mode 100644 app/views/notifications_mailer/notify_user_is_validated.html.erb create mode 100644 app/views/notifications_mailer/notify_user_proof_of_identity_refusal.html.erb create mode 100644 db/migrate/20220422090245_create_proof_of_identity_types.rb create mode 100644 db/migrate/20220422090709_create_proof_of_identity_types_groups.rb create mode 100644 db/migrate/20220426162334_create_proof_of_identity_files.rb create mode 100644 db/migrate/20220428123828_create_proof_of_identity_refusals.rb create mode 100644 db/migrate/20220428125751_create_join_table_proof_of_identity_type_proof_of_identity_refusal.rb create mode 100644 db/migrate/20220429164234_add_validated_at_to_user.rb create mode 100644 db/migrate/20220506143526_create_profile_custom_fields.rb create mode 100644 db/migrate/20220509105714_create_user_profile_custom_fields.rb diff --git a/.dockerignore b/.dockerignore index d1d9e2d39..11faf34e4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -31,6 +31,9 @@ imports # accounting archives accounting +# Proof of identity files +proof_of_identity_files + # Development files Vagrantfile provision diff --git a/.gitignore b/.gitignore index 4a28e3c41..3673b932c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ # Archives of closed accounting periods /accounting/* +# Proof of identity files +/proof_of_identity_files/* + .DS_Store .vagrant diff --git a/CHANGELOG.md b/CHANGELOG.md index e9159517e..e6fd371a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Dockerfile b/Dockerfile index f8381188c..a47795225 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/app/controllers/api/members_controller.rb b/app/controllers/api/members_controller.rb index c5490e889..87d405f5a 100644 --- a/app/controllers/api/members_controller.rb +++ b/app/controllers/api/members_controller.rb @@ -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, diff --git a/app/controllers/api/profile_custom_fields_controller.rb b/app/controllers/api/profile_custom_fields_controller.rb new file mode 100644 index 000000000..1d45e5985 --- /dev/null +++ b/app/controllers/api/profile_custom_fields_controller.rb @@ -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 diff --git a/app/controllers/api/proof_of_identity_files_controller.rb b/app/controllers/api/proof_of_identity_files_controller.rb new file mode 100644 index 000000000..32a8679cd --- /dev/null +++ b/app/controllers/api/proof_of_identity_files_controller.rb @@ -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 diff --git a/app/controllers/api/proof_of_identity_refusals_controller.rb b/app/controllers/api/proof_of_identity_refusals_controller.rb new file mode 100644 index 000000000..e61be3b7b --- /dev/null +++ b/app/controllers/api/proof_of_identity_refusals_controller.rb @@ -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 diff --git a/app/controllers/api/proof_of_identity_types_controller.rb b/app/controllers/api/proof_of_identity_types_controller.rb new file mode 100644 index 000000000..5a3f9e0eb --- /dev/null +++ b/app/controllers/api/proof_of_identity_types_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1301763bd..5ee5c20fc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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] diff --git a/app/frontend/src/javascript/api/member.ts b/app/frontend/src/javascript/api/member.ts index d01c4be93..b2727572b 100644 --- a/app/frontend/src/javascript/api/member.ts +++ b/app/frontend/src/javascript/api/member.ts @@ -39,4 +39,9 @@ export default class MemberAPI { const res: AxiosResponse = await apiClient.get('/api/members/current'); return res?.data; } + + static async validate (member: User): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/members/${member.id}/validate`, { user: member }); + return res?.data; + } } diff --git a/app/frontend/src/javascript/api/profile-custom-field.ts b/app/frontend/src/javascript/api/profile-custom-field.ts new file mode 100644 index 000000000..0267dadd4 --- /dev/null +++ b/app/frontend/src/javascript/api/profile-custom-field.ts @@ -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> { + const res: AxiosResponse> = await apiClient.get('/api/profile_custom_fields'); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/profile_custom_fields/${id}`); + return res?.data; + } + + static async create (profileCustomField: ProfileCustomField): Promise { + const res: AxiosResponse = await apiClient.post('/api/profile_custom_fields', { profile_custom_field: profileCustomField }); + return res?.data; + } + + static async update (profileCustomField: ProfileCustomField): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/profile_custom_fields/${profileCustomField.id}`, { profile_custom_field: profileCustomField }); + return res?.data; + } + + static async destroy (profileCustomFieldId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/profile_custom_fields/${profileCustomFieldId}`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/api/proof-of-identity-file.ts b/app/frontend/src/javascript/api/proof-of-identity-file.ts new file mode 100644 index 000000000..1fa1c192f --- /dev/null +++ b/app/frontend/src/javascript/api/proof-of-identity-file.ts @@ -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> { + const res: AxiosResponse> = await apiClient.get(`/api/proof_of_identity_files${this.filtersToQuery(filters)}`); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/proof_of_identity_files/${id}`); + return res?.data; + } + + static async create (proofOfIdentityFile: FormData): Promise { + const res: AxiosResponse = await apiClient.post('/api/proof_of_identity_files', proofOfIdentityFile); + return res?.data; + } + + static async update (id: number, proofOfIdentityFile: FormData): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/proof_of_identity_files/${id}`, proofOfIdentityFile); + return res?.data; + } + + static async destroy (proofOfIdentityFileId: number): Promise { + const res: AxiosResponse = 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('&'); + } +} diff --git a/app/frontend/src/javascript/api/proof-of-identity-refusal.ts b/app/frontend/src/javascript/api/proof-of-identity-refusal.ts new file mode 100644 index 000000000..efa8d5a77 --- /dev/null +++ b/app/frontend/src/javascript/api/proof-of-identity-refusal.ts @@ -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> { + const res: AxiosResponse> = await apiClient.get(`/api/proof_of_identity_refusals${this.filtersToQuery(filters)}`); + return res?.data; + } + + static async create (proofOfIdentityRefusal: ProofOfIdentityRefusal): Promise { + const res: AxiosResponse = 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('&'); + } +} diff --git a/app/frontend/src/javascript/api/proof-of-identity-type.ts b/app/frontend/src/javascript/api/proof-of-identity-type.ts new file mode 100644 index 000000000..f44fda060 --- /dev/null +++ b/app/frontend/src/javascript/api/proof-of-identity-type.ts @@ -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> { + const res: AxiosResponse> = await apiClient.get(`/api/proof_of_identity_types${this.filtersToQuery(filters)}`); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/proof_of_identity_types/${id}`); + return res?.data; + } + + static async create (proofOfIdentityType: ProofOfIdentityType): Promise { + const res: AxiosResponse = await apiClient.post('/api/proof_of_identity_types', { proof_of_identity_type: proofOfIdentityType }); + return res?.data; + } + + static async update (proofOfIdentityType: ProofOfIdentityType): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/proof_of_identity_types/${proofOfIdentityType.id}`, { proof_of_identity_type: proofOfIdentityType }); + return res?.data; + } + + static async destroy (proofOfIdentityTypeId: number): Promise { + const res: AxiosResponse = 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('&'); + } +} diff --git a/app/frontend/src/javascript/components/machines/machine-card.tsx b/app/frontend/src/javascript/components/machines/machine-card.tsx index f35a4fbe7..3059552a3 100644 --- a/app/frontend/src/javascript/components/machines/machine-card.tsx +++ b/app/frontend/src/javascript/components/machines/machine-card.tsx @@ -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 = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested }) => { +const MachineCardComponent: React.FC = ({ 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 = ({ user, machine, onSho onReserveMachine={handleReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} + canProposePacks={canProposePacks} className="reserve-button"> {t('app.public.machine_card.book')} @@ -80,10 +82,10 @@ const MachineCardComponent: React.FC = ({ user, machine, onSho ); }; -export const MachineCard: React.FC = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested }) => { +export const MachineCard: React.FC = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => { return ( - + ); }; diff --git a/app/frontend/src/javascript/components/machines/machines-list.tsx b/app/frontend/src/javascript/components/machines/machines-list.tsx index adf1f0f67..caca4c710 100644 --- a/app/frontend/src/javascript/components/machines/machines-list.tsx +++ b/app/frontend/src/javascript/components/machines/machines-list.tsx @@ -18,12 +18,13 @@ interface MachinesListProps { onReserveMachine: (machine: Machine) => void, onLoginRequested: () => Promise, onEnrollRequested: (trainingId: number) => void, + canProposePacks: boolean, } /** * This component shows a list of all machines and allows filtering on that list. */ -const MachinesList: React.FC = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user }) => { +const MachinesList: React.FC = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user, canProposePacks }) => { // shown machines const [machines, setMachines] = useState>(null); // we keep the full list of machines, for filtering @@ -68,19 +69,20 @@ const MachinesList: React.FC = ({ onError, onSuccess, onShowM onError={onError} onSuccess={onSuccess} onLoginRequested={onLoginRequested} - onEnrollRequested={onEnrollRequested} />; + onEnrollRequested={onEnrollRequested} + canProposePacks={canProposePacks}/>; })} ); }; -const MachinesListWrapper: React.FC = ({ user, onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested }) => { +const MachinesListWrapper: React.FC = ({ user, onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, canProposePacks }) => { return ( - + ); }; -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'])); diff --git a/app/frontend/src/javascript/components/machines/reserve-button.tsx b/app/frontend/src/javascript/components/machines/reserve-button.tsx index f3262986b..1a0bb6bea 100644 --- a/app/frontend/src/javascript/components/machines/reserve-button.tsx +++ b/app/frontend/src/javascript/components/machines/reserve-button.tsx @@ -24,13 +24,14 @@ interface ReserveButtonProps { onReserveMachine: (machine: Machine) => void, onLoginRequested: () => Promise, 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 = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children }) => { +const ReserveButtonComponent: React.FC = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => { const { t } = useTranslation('shared'); const [machine, setMachine] = useState(null); @@ -146,7 +147,7 @@ const ReserveButtonComponent: React.FC = ({ 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 = ({ currentUser, mac ); }; -export const ReserveButton: React.FC = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children }) => { +export const ReserveButton: React.FC = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => { return ( - + {children} ); }; -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'])); diff --git a/app/frontend/src/javascript/components/plans/plan-card.tsx b/app/frontend/src/javascript/components/plans/plan-card.tsx index 7178bac19..50f3c9a22 100644 --- a/app/frontend/src/javascript/components/plans/plan-card.tsx +++ b/app/frontend/src/javascript/components/plans/plan-card.tsx @@ -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 = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => { +const PlanCardComponent: React.FC = ({ 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 = ({ 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 = ({ plan, userId, subscribedPl ); }; -export const PlanCard: React.FC = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => { +export const PlanCard: React.FC = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => { return ( - + ); }; diff --git a/app/frontend/src/javascript/components/plans/plans-list.tsx b/app/frontend/src/javascript/components/plans/plans-list.tsx index 5dced13dd..e49f8a9e8 100644 --- a/app/frontend/src/javascript/components/plans/plans-list.tsx +++ b/app/frontend/src/javascript/components/plans/plans-list.tsx @@ -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>>; /** * This component display an organized list of plans to allow the end-user to select one and subscribe online */ -const PlansList: React.FC = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId }) => { +const PlansList: React.FC = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId, canSelectPlan }) => { // all plans const [plans, setPlans] = useState(null); // all plan-categories, ordered by weight @@ -218,6 +219,7 @@ const PlansList: React.FC = ({ onError, onPlanSelection, onLogin operator={operator} isSelected={isSelectedPlan(plan)} onSelectPlan={handlePlanSelection} + canSelectPlan={canSelectPlan} onLoginRequested={onLoginRequest} /> ))} @@ -239,12 +241,12 @@ const PlansList: React.FC = ({ onError, onPlanSelection, onLogin ); }; -const PlansListWrapper: React.FC = ({ customer, onError, onPlanSelection, onLoginRequest, operator, subscribedPlanId }) => { +const PlansListWrapper: React.FC = ({ customer, onError, onPlanSelection, onLoginRequest, operator, subscribedPlanId, canSelectPlan }) => { return ( - + ); }; -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'])); diff --git a/app/frontend/src/javascript/components/profile-custom-fields/profile-custom-fields-list.tsx b/app/frontend/src/javascript/components/profile-custom-fields/profile-custom-fields-list.tsx new file mode 100644 index 000000000..c3f388521 --- /dev/null +++ b/app/frontend/src/javascript/components/profile-custom-fields/profile-custom-fields-list.tsx @@ -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 = ({ onSuccess, onError }) => { + const { t } = useTranslation('admin'); + + const [profileCustomFields, setProfileCustomFields] = useState>([]); + const [profileCustomFieldToEdit, setProfileCustomFieldToEdit] = useState(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 ( + + + + + + + + + + {profileCustomFields.map(field => { + return ( + + + + + + ); + })} + +
+ {profileCustomFieldToEdit?.id !== field.id && field.label} + {profileCustomFieldToEdit?.id !== field.id && ( + + )} + {profileCustomFieldToEdit?.id === field.id && ( +
+ + + + + +
+ )} +
+ + + + + +
+ ); +}; + +const ProfileCustomFieldsListWrapper: React.FC = ({ onSuccess, onError }) => { + return ( + + + + ); +}; + +Application.Components.component('profileCustomFieldsList', react2angular(ProfileCustomFieldsListWrapper, ['onSuccess', 'onError'])); diff --git a/app/frontend/src/javascript/components/proof-of-identity/delete-proof-of-identity-type-modal.tsx b/app/frontend/src/javascript/components/proof-of-identity/delete-proof-of-identity-type-modal.tsx new file mode 100644 index 000000000..33d6fbb47 --- /dev/null +++ b/app/frontend/src/javascript/components/proof-of-identity/delete-proof-of-identity-type-modal.tsx @@ -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 = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypeId, onError }) => { + const { t } = useTranslation('admin'); + + const handleDeleteProofOfIdentityType = async (): Promise => { + 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 ( + +

{t('app.admin.settings.compte.do_you_really_want_to_delete_this_proof_of_identity_type')}

+
+ ); +}; diff --git a/app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-files.tsx b/app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-files.tsx new file mode 100644 index 000000000..1eac9b8a1 --- /dev/null +++ b/app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-files.tsx @@ -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 = ({ currentUser, onSuccess, onError }) => { + const { t } = useTranslation('admin'); + + // list of proof of identity type + const [proofOfIdentityTypes, setProofOfIdentityTypes] = useState>([]); + const [proofOfIdentityFiles, setProofOfIdentityFiles] = useState>([]); + const [files, setFiles] = useState({}); + const [errors, setErrors] = useState>([]); + + // 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(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 ( +
+

{t('app.admin.members_edit.proof_of_identity_files')}

+

{t('app.admin.members_edit.my_documents_info')}

+
+ +
+
+ {proofOfIdentityTypes.map((poit: ProofOfIdentityType) => { + return ( +
+ +
+
+ {hasFile(poit.id) && ( +
+ {files[poit.id]?.name || getProofOfIdentityFileByType(poit.id).attachment} +
+ )} + {getProofOfIdentityFileByType(poit.id) && !files[poit.id] && ( + + )} +
+ + {!hasFile(poit.id) && ( + Parcourir + )} + {hasFile(poit.id) && ( + Modifier + )} + + +
+ {errors.includes(poit.id) && {t('app.admin.members_edit.proof_of_identity_file_size_error')}} +
+ ); + })} +
+ {hasProofOfIdentityTypes() && ( + + )} +
+ ); +}; + +const ProofOfIdentityFilesWrapper: React.FC = ({ currentUser, onSuccess, onError }) => { + return ( + + + + ); +}; + +Application.Components.component('proofOfIdentityFiles', react2angular(ProofOfIdentityFilesWrapper, ['currentUser', 'onSuccess', 'onError'])); diff --git a/app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-refusal-form.tsx b/app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-refusal-form.tsx new file mode 100644 index 000000000..8f065fa90 --- /dev/null +++ b/app/frontend/src/javascript/components/proof-of-identity/proof-of-identity-refusal-form.tsx @@ -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, + onChange: (field: string, value: string | Array) => void, +} + +/** + * Form to set the stripe's public and private keys + */ +export const ProofOfIdentityRefusalForm: React.FC = ({ proofOfIdentityTypes, onChange }) => { + const { t } = useTranslation('admin'); + + const [values, setValues] = useState>([]); + const [message, setMessage] = useState(''); + + /** + * 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; + 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 ( +
+
+
+ {proofOfIdentityTypes.map(type =>
+ + +
)} +
+
+ +