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:
parent
6e4031523c
commit
302c55755e
@ -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
|
||||
|
10
app/frontend/src/javascript/api/tag.ts
Normal file
10
app/frontend/src/javascript/api/tag.ts
Normal 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;
|
||||
}
|
||||
}
|
16
app/frontend/src/javascript/api/training.ts
Normal file
16
app/frontend/src/javascript/api/training.ts
Normal 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('&');
|
||||
}
|
||||
}
|
@ -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]);
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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']));
|
||||
|
@ -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();
|
||||
}
|
||||
|
4
app/frontend/src/javascript/models/tag.ts
Normal file
4
app/frontend/src/javascript/models/tag.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Tag {
|
||||
id?: number,
|
||||
name: string,
|
||||
}
|
18
app/frontend/src/javascript/models/training.ts
Normal file
18
app/frontend/src/javascript/models/training.ts
Normal 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'],
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
23
app/services/training_service.rb
Normal file
23
app/services/training_service.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user