1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +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]
def index
@requested_attributes = params[:requested_attributes]
@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')
@trainings = TrainingService.list(params)
end
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(() => {
setIsDirty(_get(formState?.dirtyFields, id));
console.log(_get(formState?.dirtyFields, id));
setFieldError(_get(formState?.errors, id));
}, [formState]);

View File

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

View File

@ -19,11 +19,13 @@ import { EditSocials } from '../socials/edit-socials';
import UserLib from '../../lib/user';
import AuthProviderAPI from '../../api/auth-provider';
import { FormSelect } from '../form/form-select';
import { Group } from '../../models/group';
import GroupAPI from '../../api/group';
import CustomAssetAPI from '../../api/custom-asset';
import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
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;
@ -36,6 +38,8 @@ interface UserProfileFormProps {
onSuccess: (user: User) => void,
showGroupInput?: 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
*/
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');
// 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 [isLocalDatabaseProvider, setIsLocalDatabaseProvider] = useState<boolean>(false);
const [groups, setGroups] = useState<Group[]>([]);
const [groups, setGroups] = useState<selectOption[]>([]);
const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null);
const [trainings, setTrainings] = useState<selectOption[]>([]);
const [tags, setTags] = useState<selectOption[]>([]);
useEffect(() => {
AuthProviderAPI.active().then(data => {
setIsLocalDatabaseProvider(data.providable_type === 'DatabaseProvider');
}).catch(error => onError(error));
GroupAPI.index({ disabled: false, admins: user.role === 'admin' }).then(data => {
setGroups(data);
}).catch(error => onError(error));
CustomAssetAPI.get(CustomAssetName.CguFile).then(data => {
setTermsAndConditions(data);
}).catch(error => onError(error));
if (showGroupInput) {
GroupAPI.index({ disabled: false, admins: user.role === 'admin' }).then(data => {
setGroups(buildOptions(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> => {
return groups.map(t => {
const buildOptions = (items: Array<{ id?: number, name: string }>): Array<selectOption> => {
return items.map(t => {
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')} />
</div>
{showGroupInput && <div className="group">
<FormSelect options={buildGroupsOptions()}
<FormSelect options={groups}
control={control}
id="group_id"
rules={{ required: true }}
@ -296,6 +314,20 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
formState={formState}
label={t('app.shared.user_profile_form.group')} />
</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">
<FormSwitch control={control}
disabled={isDisabled}
@ -315,7 +347,11 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
};
UserProfileForm.defaultProps = {
size: 'large'
size: 'large',
showGroupInput: false,
showTrainingsInput: false,
showTermsAndConditionsInput: false,
showTagsInput: false
};
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;
// The user to edit
$scope.user = memberPromise;
$scope.user = cleanUser(memberPromise);
// Should the password be modified?
$scope.password = { change: false };
@ -813,6 +813,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
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) {
const modalInstance = $uibModal.open({
animation: true,
@ -914,6 +922,13 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
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
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,
gender: string,
birthday: TDateISODate
training_ids: Array<number>
},
subscribed_plan: Plan,
subscription: {
@ -75,7 +76,7 @@ export interface User {
}
},
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
}

View File

@ -42,22 +42,18 @@
</div>
</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 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.gender member.statistic_profile.gender.to_s
json.birthday member.statistic_profile&.birthday&.to_date&.iso8601
json.training_ids member.statistic_profile&.training_ids
end
if member.subscribed_plan

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
role = (current_user and current_user.admin?) ? 'admin' : 'user'
json.array!(@trainings) do |training|
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.plan_ids training.plan_ids if role === 'admin'
json.plan_ids training.plan_ids if role == 'admin'
end

View File

@ -988,6 +988,7 @@ en:
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_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
free_extend_modal:
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_profile: "This data will only be displayed on your profile"
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/>"
must_accept_terms: "You must accept the terms and conditions"
save: "Save"