1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-22 11:52:21 +01:00

(feat) edit/delete child in members list

This commit is contained in:
Du Peng 2023-05-31 18:20:14 +02:00
parent bc8521fc15
commit b4781b1642
10 changed files with 141 additions and 37 deletions

View File

@ -10,7 +10,7 @@ class API::ChildrenController < API::APIController
authorize Child authorize Child
user_id = current_user.id user_id = current_user.id
user_id = params[:user_id] if current_user.privileged? && params[:user_id] user_id = params[:user_id] if current_user.privileged? && params[:user_id]
@children = Child.where(user_id: user_id) @children = Child.where(user_id: user_id).includes(:supporting_document_files).order(:created_at)
end end
def show def show

View File

@ -11,7 +11,7 @@ interface ChildItemProps {
child: Child; child: Child;
size: 'sm' | 'lg'; size: 'sm' | 'lg';
onEdit: (child: Child) => void; onEdit: (child: Child) => void;
onDelete: (error: string) => void; onDelete: (child: Child, error: string) => void;
onError: (error: string) => void; onError: (error: string) => void;
} }
@ -33,8 +33,9 @@ export const ChildItem: React.FC<ChildItemProps> = ({ child, size, onEdit, onDel
const deleteChild = () => { const deleteChild = () => {
ChildAPI.destroy(child.id).then(() => { ChildAPI.destroy(child.id).then(() => {
toggleDeleteChildModal(); toggleDeleteChildModal();
onDelete(t('app.public.child_item.deleted')); onDelete(child, t('app.public.child_item.deleted'));
}).catch(() => { }).catch((e) => {
console.error(e);
onError(t('app.public.child_item.unable_to_delete')); onError(t('app.public.child_item.unable_to_delete'));
}); });
}; };

View File

@ -13,7 +13,7 @@ interface ChildModalProps {
operator: User; operator: User;
isOpen: boolean; isOpen: boolean;
toggleModal: () => void; toggleModal: () => void;
onSuccess: (msg: string) => void; onSuccess: (child: Child, msg: string) => void;
onError: (error: string) => void; onError: (error: string) => void;
supportingDocumentsTypes: Array<SupportingDocumentType>; supportingDocumentsTypes: Array<SupportingDocumentType>;
} }
@ -35,7 +35,7 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
await ChildAPI.create(data); await ChildAPI.create(data);
} }
toggleModal(); toggleModal();
onSuccess(''); onSuccess(data, '');
} catch (error) { } catch (error) {
onError(error); onError(error);
} }

View File

@ -70,7 +70,7 @@ export const ChildrenDashboard: React.FC<ChildrenDashboardProps> = ({ user, oper
/** /**
* Delete a child * Delete a child
*/ */
const handleDeleteChildSuccess = (msg: string) => { const handleDeleteChildSuccess = (_child: Child, msg: string) => {
ChildAPI.index({ user_id: user.id }).then(setChildren); ChildAPI.index({ user_id: user.id }).then(setChildren);
onSuccess(msg); onSuccess(msg);
}; };
@ -78,7 +78,7 @@ export const ChildrenDashboard: React.FC<ChildrenDashboardProps> = ({ user, oper
/** /**
* Handle save child success from the API * Handle save child success from the API
*/ */
const handleSaveChildSuccess = (msg: string) => { const handleSaveChildSuccess = (_data: Child, msg: string) => {
ChildAPI.index({ user_id: user.id }).then(setChildren); ChildAPI.index({ user_id: user.id }).then(setChildren);
if (msg) { if (msg) {
onSuccess(msg); onSuccess(msg);

View File

@ -1,32 +1,28 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Member } from '../../models/member'; import { Member } from '../../models/member';
import { Child } from '../../models/child'; import { Child } from '../../models/child';
import ChildAPI from '../../api/child';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import { CaretDown, User, Users } from 'phosphor-react'; import { CaretDown, User, Users, PencilSimple, Trash } from 'phosphor-react';
import { ChildItem } from '../family-account/child-item'; import { ChildItem } from '../family-account/child-item';
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
interface MembersListItemProps { interface MembersListItemProps {
member: Member, member: Member,
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (message: string) => void onSuccess: (message: string) => void
onEditChild: (child: Child) => void;
onDeleteChild: (child: Child, error: string) => void;
onDeleteMember: (memberId: number) => void;
} }
/** /**
* Members list * Members list
*/ */
export const MembersListItem: React.FC<MembersListItemProps> = ({ member, onError, onSuccess }) => { export const MembersListItem: React.FC<MembersListItemProps> = ({ member, onError, onEditChild, onDeleteChild, onDeleteMember }) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');
const [children, setChildren] = useState<Array<Child>>([]);
const [childrenList, setChildrenList] = useState(false); const [childrenList, setChildrenList] = useState(false);
useEffect(() => {
ChildAPI.index({ user_id: member.id }).then(setChildren);
}, [member]);
/** /**
* Redirect to the given user edition page * Redirect to the given user edition page
*/ */
@ -38,12 +34,12 @@ export const MembersListItem: React.FC<MembersListItemProps> = ({ member, onErro
<div key={member.id} className={`members-list-item ${member.validated_at ? 'is-validated' : ''} ${member.need_completion ? 'is-incomplet' : ''}`}> <div key={member.id} className={`members-list-item ${member.validated_at ? 'is-validated' : ''} ${member.need_completion ? 'is-incomplet' : ''}`}>
<div className="left-col"> <div className="left-col">
<div className='status'> <div className='status'>
{(children.length > 0) {(member.children.length > 0)
? <Users size={24} weight="bold" /> ? <Users size={24} weight="bold" />
: <User size={24} weight="bold" /> : <User size={24} weight="bold" />
} }
</div> </div>
{(children.length > 0) && {(member.children.length > 0) &&
<FabButton onClick={() => setChildrenList(!childrenList)} className={`toggle ${childrenList ? 'open' : ''}`}> <FabButton onClick={() => setChildrenList(!childrenList)} className={`toggle ${childrenList ? 'open' : ''}`}>
<CaretDown size={24} weight="bold" /> <CaretDown size={24} weight="bold" />
</FabButton> </FabButton>
@ -78,22 +74,21 @@ export const MembersListItem: React.FC<MembersListItemProps> = ({ member, onErro
</div> </div>
</div> </div>
<div className="member-actions"> <div className="member-actions edit-destroy-buttons">
{/* TODO: <EditDestroyButtons> */} <FabButton onClick={() => toMemberEdit(member.id)} className="edit-btn">
<EditDestroyButtons onError={onError} <PencilSimple size={20} weight="fill" />
onEdit={() => toMemberEdit(member.id)} </FabButton>
onDeleteSuccess={() => onSuccess} <FabButton onClick={() => onDeleteMember(member.id)} className="delete-btn">
itemId={member.id} <Trash size={20} weight="fill" />
itemType={t('app.admin.members_list_item.item_type')} </FabButton>
destroy={() => new Promise(() => console.log(`Delete member ${member.id}`))} />
</div> </div>
</div> </div>
{ (children.length > 0) && { (member.children.length > 0) &&
<div className={`member-children ${childrenList ? 'open' : ''}`}> <div className={`member-children ${childrenList ? 'open' : ''}`}>
<hr /> <hr />
{children.map(child => ( {member.children.map((child: Child) => (
<ChildItem key={child.id} child={child} size='sm' onEdit={() => console.log('edit child')} onDelete={() => console.log('delete child')} onError={onError} /> <ChildItem key={child.id} child={child} size='sm' onEdit={onEditChild} onDelete={onDeleteChild} onError={onError} />
))} ))}
</div> </div>
} }

View File

@ -1,27 +1,79 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import { Loader } from '../base/loader'; import { Loader } from '../base/loader';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { Member } from '../../models/member'; import { Member } from '../../models/member';
import { MembersListItem } from './members-list-item'; import { MembersListItem } from './members-list-item';
import { SupportingDocumentType } from '../../models/supporting-document-type';
import SupportingDocumentTypeAPI from '../../api/supporting-document-type';
import { Child } from '../../models/child';
import { ChildModal } from '../family-account/child-modal';
import { User } from '../../models/user';
declare const Application: IApplication; declare const Application: IApplication;
interface MembersListProps { interface MembersListProps {
members: Member[], members: Member[],
operator: User,
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (message: string) => void onSuccess: (message: string) => void
onDeleteMember: (memberId: number) => void;
onDeletedChild: (memberId: number, childId: number) => void;
onUpdatedChild: (memberId: number, child: Child) => void;
} }
/** /**
* Members list * Members list
*/ */
export const MembersList: React.FC<MembersListProps> = ({ members, onError, onSuccess }) => { export const MembersList: React.FC<MembersListProps> = ({ members, onError, onSuccess, operator, onDeleteMember, onDeletedChild, onUpdatedChild }) => {
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<SupportingDocumentType>>([]);
const [child, setChild] = useState<Child>();
const [isOpenChildModal, setIsOpenChildModal] = useState<boolean>(false);
useEffect(() => {
SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => {
setSupportingDocumentsTypes(tData);
});
}, []);
/**
* Open the edit child modal
*/
const editChild = (child: Child) => {
setIsOpenChildModal(true);
setChild({
...child,
supporting_document_files_attributes: supportingDocumentsTypes.map(t => {
const file = child.supporting_document_files_attributes.find(f => f.supporting_document_type_id === t.id);
return file || { supporting_document_type_id: t.id };
})
} as Child);
};
/**
* Delete a child
*/
const handleDeleteChildSuccess = (c: Child, msg: string) => {
onDeletedChild(c.user_id, c.id);
onSuccess(msg);
};
/**
* Handle save child success from the API
*/
const handleSaveChildSuccess = (c: Child, msg: string) => {
onUpdatedChild(c.user_id, c);
if (msg) {
onSuccess(msg);
}
};
return ( return (
<div className="members-list"> <div className="members-list">
{members.map(member => ( {members.map(member => (
<MembersListItem key={member.id} member={member} onError={onError} onSuccess={onSuccess} /> <MembersListItem key={member.id} member={member} onError={onError} onSuccess={onSuccess} onDeleteMember={onDeleteMember} onEditChild={editChild} onDeleteChild={handleDeleteChildSuccess} />
))} ))}
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} />
</div> </div>
); );
}; };
@ -34,4 +86,4 @@ const MembersListWrapper: React.FC<MembersListProps> = (props) => {
); );
}; };
Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess'])); Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess', 'operator', 'onDeleteMember', 'onDeletedChild', 'onUpdatedChild']));

View File

@ -291,7 +291,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
Member.delete( Member.delete(
{ id: memberId }, { id: memberId },
function () { function () {
$scope.members.splice(findItemIdxById($scope.members, memberId), 1); $scope.members = _.filter($scope.members, function (m) { return m.id !== memberId; });
return growl.success(_t('app.admin.members.member_successfully_deleted')); return growl.success(_t('app.admin.members.member_successfully_deleted'));
}, },
function (error) { function (error) {
@ -303,6 +303,32 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
); );
}; };
$scope.onDeletedChild = function (memberId, childId) {
$scope.members = $scope.members.map(function (member) {
if (member.id === memberId) {
member.children = _.filter(member.children, function (c) { return c.id !== childId; });
return member;
}
return member;
});
};
$scope.onUpdatedChild = function (memberId, child) {
$scope.members = $scope.members.map(function (member) {
if (member.id === memberId) {
member.children = member.children.map(function (c) {
if (c.id === child.id) {
return child;
}
return c;
});
console.log(member.children);
return member;
}
return member;
});
};
/** /**
* Ask for confirmation then delete the specified administrator * Ask for confirmation then delete the specified administrator
* @param admins {Array} full list of administrators * @param admins {Array} full list of administrators
@ -588,6 +614,20 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
} }
}; };
/**
* Callback triggered in case of error
*/
$scope.onError = (message) => {
growl.error(message);
};
/**
* Callback triggered in case of success
*/
$scope.onSuccess = (message) => {
growl.success(message);
};
/* PRIVATE SCOPE */ /* PRIVATE SCOPE */
/** /**

View File

@ -1,4 +1,5 @@
import { TDateISO } from '../typings/date-iso'; import { TDateISO } from '../typings/date-iso';
import { Child } from './child';
export interface Member { export interface Member {
maxMembers: number maxMembers: number
@ -16,6 +17,7 @@ export interface Member {
} }
subscribed_plan?: Plan subscribed_plan?: Plan
validated_at: TDateISO validated_at: TDateISO
children: Child[]
} }
interface Plan { interface Plan {

View File

@ -35,7 +35,9 @@
<iframe name="export-frame" height="0" width="0" class="none"></iframe> <iframe name="export-frame" height="0" width="0" class="none"></iframe>
</div> </div>
<members-list members="members" on-success="onSuccess" onError="onError" /> <div>
<members-list members="members" on-success="onSuccess" on-error="onError" operator="currentUser" on-delete-member="deleteMember" on-deleted-child="onDeletedChild" on-updated-child="onUpdatedChild" />
</div>
<div class="text-center"> <div class="text-center">
<button class="btn btn-warning show-more" ng-click="showNextMembers()" ng-hide="member.noMore"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'app.admin.members.display_more_users' | translate }}</button> <button class="btn btn-warning show-more" ng-click="showNextMembers()" ng-hide="member.noMore"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'app.admin.members.display_more_users' | translate }}</button>

View File

@ -18,4 +18,16 @@ json.array!(@members) do |member|
end end
end end
json.validated_at member.validated_at json.validated_at member.validated_at
json.children member.children.order(:created_at) do |child|
json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id, :validated_at
json.supporting_document_files_attributes child.supporting_document_files do |f|
json.id f.id
json.supportable_id f.supportable_id
json.supportable_type f.supportable_type
json.supporting_document_type_id f.supporting_document_type_id
json.attachment f.attachment.file&.filename
json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_identifier ? "/api/supporting_document_files/#{f.id}/download" : nil
end
end
end end