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

(ui) admin edit user: use new user-profile-form component

This commit is contained in:
Sylvain 2022-05-09 17:18:49 +02:00
parent 6e4031523c
commit 302c55755e
16 changed files with 163 additions and 42 deletions

View File

@ -8,14 +8,7 @@ class API::TrainingsController < API::ApiController
before_action :set_training, only: %i[update destroy] before_action :set_training, only: %i[update destroy]
def index def index
@requested_attributes = params[:requested_attributes] @trainings = TrainingService.list(params)
@trainings = policy_scope(Training)
@trainings = @trainings.where(public_page: true) if params[:public_page]
return unless attribute_requested?(@requested_attributes, 'availabilities')
@trainings = @trainings.includes(availabilities: [slots: [reservation: [user: %i[profile trainings]]]])
.order('availabilities.start_at DESC')
end end
def show def show

View File

@ -0,0 +1,10 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { Tag } from '../models/tag';
export default class TagAPI {
static async index (): Promise<Array<Tag>> {
const res: AxiosResponse<Array<Tag>> = await apiClient.get('/api/tags');
return res?.data;
}
}

View File

@ -0,0 +1,16 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { Training, TrainingIndexFilter } from '../models/training';
export default class TrainingAPI {
static async index (filters?: TrainingIndexFilter): Promise<Array<Training>> {
const res: AxiosResponse<Array<Training>> = await apiClient.get(`/api/trainings${this.filtersToQuery(filters)}`);
return res?.data;
}
private static filtersToQuery (filters?: TrainingIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -24,7 +24,6 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
useEffect(() => { useEffect(() => {
setIsDirty(_get(formState?.dirtyFields, id)); setIsDirty(_get(formState?.dirtyFields, id));
console.log(_get(formState?.dirtyFields, id));
setFieldError(_get(formState?.errors, id)); setFieldError(_get(formState?.errors, id));
}, [formState]); }, [formState]);

View File

@ -39,6 +39,10 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
} }
}, [disabled]); }, [disabled]);
useEffect(() => {
setAllOptions(options);
}, [options]);
/** /**
* The following callback will trigger the onChange callback, if it was passed to this component, * The following callback will trigger the onChange callback, if it was passed to this component,
* when the selected option changes. * when the selected option changes.

View File

@ -19,11 +19,13 @@ import { EditSocials } from '../socials/edit-socials';
import UserLib from '../../lib/user'; import UserLib from '../../lib/user';
import AuthProviderAPI from '../../api/auth-provider'; import AuthProviderAPI from '../../api/auth-provider';
import { FormSelect } from '../form/form-select'; import { FormSelect } from '../form/form-select';
import { Group } from '../../models/group';
import GroupAPI from '../../api/group'; import GroupAPI from '../../api/group';
import CustomAssetAPI from '../../api/custom-asset'; import CustomAssetAPI from '../../api/custom-asset';
import { CustomAsset, CustomAssetName } from '../../models/custom-asset'; import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
import { HtmlTranslate } from '../base/html-translate'; import { HtmlTranslate } from '../base/html-translate';
import TrainingAPI from '../../api/training';
import TagAPI from '../../api/tag';
import { FormMultiSelect } from '../form/form-multi-select';
declare const Application: IApplication; declare const Application: IApplication;
@ -36,6 +38,8 @@ interface UserProfileFormProps {
onSuccess: (user: User) => void, onSuccess: (user: User) => void,
showGroupInput?: boolean, showGroupInput?: boolean,
showTermsAndConditionsInput?: boolean, showTermsAndConditionsInput?: boolean,
showTrainingsInput?: boolean,
showTagsInput?: boolean,
} }
/** /**
@ -47,7 +51,7 @@ type selectOption = { value: number, label: string };
/** /**
* Form component to create or update a user * Form component to create or update a user
*/ */
export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size, user, className, onError, onSuccess, showGroupInput = false, showTermsAndConditionsInput = false }) => { export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size, user, className, onError, onSuccess, showGroupInput, showTermsAndConditionsInput, showTrainingsInput, showTagsInput }) => {
const { t } = useTranslation('shared'); const { t } = useTranslation('shared');
// regular expression to validate the input fields // regular expression to validate the input fields
@ -59,26 +63,40 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
const [isOrganization, setIsOrganization] = useState<boolean>(!_isNil(user.invoicing_profile_attributes.organization_attributes)); const [isOrganization, setIsOrganization] = useState<boolean>(!_isNil(user.invoicing_profile_attributes.organization_attributes));
const [isLocalDatabaseProvider, setIsLocalDatabaseProvider] = useState<boolean>(false); const [isLocalDatabaseProvider, setIsLocalDatabaseProvider] = useState<boolean>(false);
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<selectOption[]>([]);
const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null); const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null);
const [trainings, setTrainings] = useState<selectOption[]>([]);
const [tags, setTags] = useState<selectOption[]>([]);
useEffect(() => { useEffect(() => {
AuthProviderAPI.active().then(data => { AuthProviderAPI.active().then(data => {
setIsLocalDatabaseProvider(data.providable_type === 'DatabaseProvider'); setIsLocalDatabaseProvider(data.providable_type === 'DatabaseProvider');
}).catch(error => onError(error)); }).catch(error => onError(error));
GroupAPI.index({ disabled: false, admins: user.role === 'admin' }).then(data => { if (showGroupInput) {
setGroups(data); GroupAPI.index({ disabled: false, admins: user.role === 'admin' }).then(data => {
}).catch(error => onError(error)); setGroups(buildOptions(data));
CustomAssetAPI.get(CustomAssetName.CguFile).then(data => { }).catch(error => onError(error));
setTermsAndConditions(data); }
}).catch(error => onError(error)); if (showTermsAndConditionsInput) {
CustomAssetAPI.get(CustomAssetName.CguFile).then(setTermsAndConditions).catch(error => onError(error));
}
if (showTrainingsInput) {
TrainingAPI.index({ disabled: false }).then(data => {
setTrainings(buildOptions(data));
}).catch(error => onError(error));
}
if (showTagsInput) {
TagAPI.index().then(data => {
setTags(buildOptions(data));
}).catch(error => onError(error));
}
}, []); }, []);
/** /**
* Convert all groups to the react-select format * Convert the provided array of items to the react-select format
*/ */
const buildGroupsOptions = (): Array<selectOption> => { const buildOptions = (items: Array<{ id?: number, name: string }>): Array<selectOption> => {
return groups.map(t => { return items.map(t => {
return { value: t.id, label: t.name }; return { value: t.id, label: t.name };
}); });
}; };
@ -288,7 +306,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
tooltip={t('app.shared.user_profile_form.allow_newsletter_help')} /> tooltip={t('app.shared.user_profile_form.allow_newsletter_help')} />
</div> </div>
{showGroupInput && <div className="group"> {showGroupInput && <div className="group">
<FormSelect options={buildGroupsOptions()} <FormSelect options={groups}
control={control} control={control}
id="group_id" id="group_id"
rules={{ required: true }} rules={{ required: true }}
@ -296,6 +314,20 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
formState={formState} formState={formState}
label={t('app.shared.user_profile_form.group')} /> label={t('app.shared.user_profile_form.group')} />
</div>} </div>}
{showTrainingsInput && <div className="trainings">
<FormMultiSelect control={control}
options={trainings}
formState={formState}
label={t('app.shared.user_profile_form.trainings')}
id="statistic_profile_attributes.training_ids" />
</div>}
{showTagsInput && <div className="tags">
<FormMultiSelect control={control}
options={tags}
formState={formState}
label={t('app.shared.user_profile_form.tags')}
id="tag_ids" />
</div>}
{showTermsAndConditionsInput && termsAndConditions && <div className="terms-and-conditions"> {showTermsAndConditionsInput && termsAndConditions && <div className="terms-and-conditions">
<FormSwitch control={control} <FormSwitch control={control}
disabled={isDisabled} disabled={isDisabled}
@ -315,7 +347,11 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
}; };
UserProfileForm.defaultProps = { UserProfileForm.defaultProps = {
size: 'large' size: 'large',
showGroupInput: false,
showTrainingsInput: false,
showTermsAndConditionsInput: false,
showTagsInput: false
}; };
const UserProfileFormWrapper: React.FC<UserProfileFormProps> = (props) => { const UserProfileFormWrapper: React.FC<UserProfileFormProps> = (props) => {
@ -326,4 +362,4 @@ const UserProfileFormWrapper: React.FC<UserProfileFormProps> = (props) => {
); );
}; };
Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'className', 'onError', 'onSuccess', 'showGroupInput', 'showTermsAndConditionsInput'])); Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'className', 'onError', 'onSuccess', 'showGroupInput', 'showTermsAndConditionsInput', 'showTagsInput', 'showTrainingsInput']));

View File

@ -664,7 +664,7 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
$scope.tags = tagsPromise; $scope.tags = tagsPromise;
// The user to edit // The user to edit
$scope.user = memberPromise; $scope.user = cleanUser(memberPromise);
// Should the password be modified? // Should the password be modified?
$scope.password = { change: false }; $scope.password = { change: false };
@ -813,6 +813,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
growl.error(message); growl.error(message);
}; };
/**
* Callback triggered when the user was successfully updated
*/
$scope.onUserSuccess = (user) => {
growl.success(_t('app.admin.members_edit.update_success'));
$state.go('app.admin.members');
};
$scope.createWalletCreditModal = function (user, wallet) { $scope.createWalletCreditModal = function (user, wallet) {
const modalInstance = $uibModal.open({ const modalInstance = $uibModal.open({
animation: true, animation: true,
@ -914,6 +922,13 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
return new MembersController($scope, $state, Group, Training); return new MembersController($scope, $state, Group, Training);
}; };
// prepare the user for the react-hook-form
function cleanUser (user) {
delete user.$promise;
delete user.$resolved;
return user;
}
// !!! MUST BE CALLED AT THE END of the controller // !!! MUST BE CALLED AT THE END of the controller
return initialize(); return initialize();
} }

View File

@ -0,0 +1,4 @@
export interface Tag {
id?: number,
name: string,
}

View File

@ -0,0 +1,18 @@
export interface Training {
id?: number,
name: string,
description: string,
machine_ids: number[],
nb_total_places: number,
slug: string,
public_page?: boolean,
disabled?: boolean,
plan_ids?: number[],
training_image?: string,
}
export interface TrainingIndexFilter {
disabled?: boolean,
public_page?: boolean,
requested_attributes?: ['availabillities'],
}

View File

@ -58,6 +58,7 @@ export interface User {
id: number, id: number,
gender: string, gender: string,
birthday: TDateISODate birthday: TDateISODate
training_ids: Array<number>
}, },
subscribed_plan: Plan, subscribed_plan: Plan,
subscription: { subscription: {
@ -75,7 +76,7 @@ export interface User {
} }
}, },
training_credits: Array<number>, training_credits: Array<number>,
machine_credits: Array<{machine_id: number, hours_used: number}>, machine_credits: Array<{ machine_id: number, hours_used: number }>,
last_sign_in_at: TDateISO last_sign_in_at: TDateISO
} }

View File

@ -42,22 +42,18 @@
</div> </div>
</section> </section>
<form role="form" name="userForm" class="form-horizontal col-md-8" novalidate action="{{ actionUrl }}" ng-upload="submited(content)" upload-options-enable-rails-csrf="true"> <section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<user-profile-form user="user"
action="'update'"
on-error="onError"
on-success="onUserSuccess"
show-group-input="true"
show-tags-input="true"
show-trainings-input="true" />
</div>
</section>
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<ng-include src="'/shared/_member_form.html'"></ng-include>
<ng-include src="'/admin/members/_form.html'"></ng-include>
</div> <!-- ./panel-body -->
<div class="panel-footer no-padder">
<input type="submit" value="{{ 'app.shared.buttons.confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="userForm.$invalid"/>
</div>
</section>
</form>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'app.admin.members_edit.subscription' | translate }}" ng-if="$root.modules.plans"> <uib-tab heading="{{ 'app.admin.members_edit.subscription' | translate }}" ng-if="$root.modules.plans">

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Provides methods for Trainings
class TrainingService
def self.list(filters)
trainings = Training.includes(:training_image, :plans, :machines)
if filters[:disabled].present?
state = filters[:disabled] == 'false' ? [nil, false] : true
trainings = trainings.where(disabled: state)
end
if filters[:public_page].present?
state = filters[:public_page] == 'false' ? [nil, false] : true
trainings = trainings.where(public_page: state)
end
if filters[:requested_attributes].try(:include?, 'availabilities')
trainings = trainings.includes(availabilities: [slots: [reservation: [user: %i[profile trainings]]]])
.order('availabilities.start_at DESC')
end
trainings
end
end

View File

@ -45,6 +45,7 @@ json.statistic_profile_attributes do
json.id member.statistic_profile.id json.id member.statistic_profile.id
json.gender member.statistic_profile.gender.to_s json.gender member.statistic_profile.gender.to_s
json.birthday member.statistic_profile&.birthday&.to_date&.iso8601 json.birthday member.statistic_profile&.birthday&.to_date&.iso8601
json.training_ids member.statistic_profile&.training_ids
end end
if member.subscribed_plan if member.subscribed_plan

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
role = (current_user and current_user.admin?) ? 'admin' : 'user' role = (current_user and current_user.admin?) ? 'admin' : 'user'
json.array!(@trainings) do |training| json.array!(@trainings) do |training|
json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :slug, :disabled json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :slug, :disabled
json.training_image training.training_image.attachment.large.url if training.training_image json.training_image training.training_image.attachment.large.url if training.training_image
json.plan_ids training.plan_ids if role === 'admin' json.plan_ids training.plan_ids if role == 'admin'
end end

View File

@ -988,6 +988,7 @@ en:
to_credit: 'Credit' to_credit: 'Credit'
cannot_credit_own_wallet: "You cannot credit your own wallet. Please ask another manager or an administrator to credit your wallet." cannot_credit_own_wallet: "You cannot credit your own wallet. Please ask another manager or an administrator to credit your wallet."
cannot_extend_own_subscription: "You cannot extend your own subscription. Please ask another manager or an administrator to extend your subscription." cannot_extend_own_subscription: "You cannot extend your own subscription. Please ask another manager or an administrator to extend your subscription."
update_success: "Member's profile successfully updated"
# extend a subscription for free # extend a subscription for free
free_extend_modal: free_extend_modal:
extend_subscription: "Extend the subscription" extend_subscription: "Extend the subscription"

View File

@ -73,6 +73,8 @@ en:
used_for_reservation: "This data will be used in case of change on one of your bookings" used_for_reservation: "This data will be used in case of change on one of your bookings"
used_for_profile: "This data will only be displayed on your profile" used_for_profile: "This data will only be displayed on your profile"
group: "Group" group: "Group"
trainings: "Trainings"
tags: "Tags"
terms_and_conditions_html: "I've read and accept <a href=\"{POLICY_URL}\" target=\"_blank\">the terms and conditions<a/>" terms_and_conditions_html: "I've read and accept <a href=\"{POLICY_URL}\" target=\"_blank\">the terms and conditions<a/>"
must_accept_terms: "You must accept the terms and conditions" must_accept_terms: "You must accept the terms and conditions"
save: "Save" save: "Save"