Here you will found all the necessary info about how to create the team, how to manage team creation and how to invite member an so on.
Teams are one of the main part of the application. Without it you won't be able to do nothing here. Let's start from backend.
Backend
Team
The main files responsible for team are lay in the Team module.
your_project/api/src/teams/Team entity have @OneToMany relations with Team Membership entity, Businesses entity, Leads entity, Lead Status entity which means one team will have a lot of member, businesses and leads related to this team, and from 4 to 12 lead statuses. @MenyToOne with Subscription entity - all the teams where user is owner will have his one subscription.
Alsoteam module have connection with Users Admin module.
In the entity file, where relations at, you can see that all of them have next code:
{ onDelete:'CASCADE' }
This means that if you delete your team - all the data related to this team will be lost, so be careful with it.
Team service:
Create new team
Get all the info about team and it's members
Leave and delete team
Inviting new members
Accept or decline invitation
Check team params, such as team users limit (due to the subscription tier) or how many teams you can create
Let's talk about next methods. They are important.
Leave Team method. As you can guess it responsible for leaving team. But the main part of it is new owner. When you leaving the team, you must pass the ownership to other user, and this new owner must be an active user of your team, otherwise you won't be able to pass him/her ownership.
After the team receives new owner, this team also receives new owner's subscription with all of it's abilities, such as team users limit. Here you must understand that older users of the team will stay there, newer will be deactivated. You can activate them from team management page or support team can do it.
After all manipulations, will be removed the old owner's membership in this team. It means you will leave this team.
async leaveTeam(user: JwtUserPayload, teamId: string, newOwnerId?: number) {constuserMembership=awaitthis.teamMembershipRepository.findOne({ where: { user: { id:user.id }, team: { id: teamId } }, withDeleted:true, relations: ['team','team.subscription'], });if (!userMembership) {thrownewHttpException( { message:'Error', description:'You are not a member of this team', code:'NOT_A_MEMBER', },HttpStatus.BAD_REQUEST ); }constteam=awaitthis.teamEntityRepository.findOne({ where: { id: teamId }, relations: ['memberships'], });if (!team) {thrownewHttpException( { message:'Error', description:'Team not found', code:'TEAM_NOT_FOUND', },HttpStatus.BAD_REQUEST ); }// we add check for membership id, in case archived user wants to leave the groupif (team.memberships.length===1&&team.memberships[0].id ===userMembership.id) {thrownewHttpException( { message:'Error', description:'You are the last member', code:'LAST_MEMBER', },HttpStatus.BAD_REQUEST ); }if (newOwnerId) {constnewOwner=awaitthis.teamMembershipRepository.findOne({ where: { id: newOwnerId }, relations: ['user','user.subscription','team'], });if (!newOwner) {thrownewHttpException( { message:'Update failed', description:'Member not found', code:'MEMBER_NOT_FOUND', },HttpStatus.BAD_REQUEST ); }if (newOwner.isDeleted ||newOwner.user.status ===AccountStatus.Pending) {thrownewHttpException( { message:'Error', description:'Archived or Pending status users cannot be new owners', code:'CANNOT_BE_NEW_OWNERS', },HttpStatus.BAD_REQUEST ); }constisTeamsLimitReached=awaitthis.checkTeamsLimit(newOwner.user);if (isTeamsLimitReached) {thrownewHttpException( { message:'Error', description:'User, whom you want to assign as a new owner, has reached the limit of teams and cannot be assigned as a new owner', code:'NEW_OWNER_TEAMS_LIMIT_REACHED', },HttpStatus.BAD_REQUEST ); }// if new owner has subscription, assign it to the team, if not - assign free tierif (newOwner.user.subscription) {awaitthis.subscriptionService.assignExistingSubscription(team.id,newOwner.user); } else {awaitthis.subscriptionService.assignFreeTier(team.id,newOwner.user.id); }// checking if number of users doesn't exceed the limit within the new owner's subscriptionconst { isLimitExceeded,exceedingMemberships } =awaitthis.checkTeamUsersLimit(teamId);// if limit is reached, make newer members status to inactive (isDeleted = true)if (isLimitExceeded) {for (constmemberof exceedingMemberships) {awaitthis.teamMembershipRepository.softDelete(member.id);awaitthis.teamMembershipRepository.update({ id:member.id }, { isDeleted:true }); } }// update new owner's role to Ownerawaitthis.teamMembershipService.update({ id: newOwnerId, role:TeamRole.Owner }, user);// send email to the new ownerthis.emailService.sendNewOwnerEmail(newOwner.user.email,team.name); }returnthis.teamMembershipRepository.delete(userMembership.id); }
Create Invitation method. The main thing is email what user receives. If user is already in the system, he/she will receive one email type. If user not in system - another type of email. Take a look.
async createInvitation(body: InvitationDto, user: JwtUserPayload) {if (body.role ===TeamRole.Owner) {thrownewHttpException( { message:'Error', description:"You can't add owner to an existing team", code:'ADD_OWNER_TO_TEAM', },HttpStatus.BAD_REQUEST ); }constteamId=body.teamId ||user.teamId;constteam=awaitthis.teamEntityRepository.findOne({ where: { id: teamId }, relations: ['memberships'], });constuserMembership=awaitthis.teamMembershipRepository.findOne({ where: { team: { id: teamId }, user: { id:user.id } }, });// we should check permission here, because when the user try to invite people// immedeatly after team creation he doesn't have team id and guard can't check permissionif (!userMembership ||userMembership.role ===TeamRole.Viewer) {thrownewForbiddenException('Access denied'); }constinvitedUser=awaitthis.userRepository.findOne({ where: { email:body.email }, withDeleted:true });if (!team) {thrownewHttpException( { message:'Error', description:'Team not found', code:'TEAM_NOT_FOUND', },HttpStatus.BAD_REQUEST ); }if (team.usersLimit <=team.memberships.length) {thrownewHttpException( { message:'Error', description:'Team is full', code:'TEAM_FULL', },HttpStatus.BAD_REQUEST ); }if (invitedUser) {constisMembershipValid=awaitthis.teamMembershipService.validateMembership(teamId,invitedUser.id);if (isMembershipValid) {thrownewHttpException( { message:'Error', description:'User is already a member of this team', code:'USER_ALREADY_MEMBER', },HttpStatus.BAD_REQUEST ); }awaitthis.teamMembershipService.create( { id:invitedUser.id, firstName:body.firstName, lastName:body.lastName, phone:body.phone }, teamId,body.role );this.emailService.sendExistedUserInvitationEmail(body.email,team.name);return { message:`Invitation email send`, description:'Wait for invited person to finish his registration', }; }constinvite=awaitthis.userService.createInvitedUser(body);awaitthis.teamMembershipService.create( { id:invite.user.id, firstName:body.firstName, lastName:body.lastName, phone:body.phone },team.id,body.role );this.emailService.sendInvitationEmail(body.email,invite.token,team.name);return { message:`Invitation email send`, description:'Wait for invited person to finish his registration', }; }
About emails and how this service works, please read related block of modules.
Team membership
Above we mentioned Team Membership. This membership have it's own module as you understand. You can find it in the following path.
your_project/api/src/team-memberships/
Membershipentity have @ManyToOnerelations with Userentity, Teamentity, which means every member is a user, each member will be member with many teams. @OneToOne with Businessesentity - one team member will be responsible for one business.
Deleting a membership will mean you break ties with your team, but it doesn't mean deleting the user. These are different things.
N.B. In entity file you can find first- and lastName fields. You can find similar in User entity. This is two different fields! In membership entity it's mean first name and last name, other word this is the alias, of the member of the team.
Same thing with phone number. For each team, each member can have their own, unique phone number.
@Column({ nullable:true }) phone!: string;
Why they are null or empty by default?
It's because we can invite new members from two different places. In one place, team creation page, we can enter only email and then the new team member will enter the information they want the other members to see in the team. In another place, from team management page, team owner will type all the info and chose the role of the member.
Next field will show if is person accepted the invitation or not. Every non new user of the app will see this status in Profile page and will also receive an invitation letter by email. There this can be changed - accept or decline invitation.
As you can guess, this module responsible for members of the team. After you create one you will be able to invite member to it. From this point, you will start your work in this app.
Each member will have it's own field of responsibilities and abilities.
About roles and their permissions, please read related block of modules
Team memberships service:
Memberships CRUD (many methods for different tasks)
Batch archiving and activating
Canceling subscription (from membership prospective, not actually canceling it)
Why tabs? This were made, because we have some steps of team creation, that we must go through, with invitation and even subscription change on demand.
Also we use local storage here to store and to control some processes here.
For example, if we changed the subscription to invite more members, this information from local storage will allow us to go back to the tab where we invite members and to save those emails in that form.
When we try to sign-in, we will find JoinTeam component. It will allow us to login with certain team.
InvitationForm component. Here we will invite new members. The main part of it is AntD Input.TextArea component. Here we can type multiple emails and all of them will receive letter with invitation to our team.
Special mention to the validation function. Take a closer look:
Another place from where we can get here and go through this steps is TeamSelector component of the SideMenu.
Team Management
From Team component, that responsible for team management page, we can find the information about out team members and change their personal information that used within team: alias, phone number, email etc. Also here we can invite new members and cancel the invitation.
In the page file you will find almost the same filters, sorters and search that we can find on the leads page.
N.B. Please notice the status of the members. After invitation was sent, member will have status "Pending". It will change to "Active" or "Inactive," indicating whether the person is currently logged in with this team or not.
You will be able to set team role when inviting one. This role is crucial on what member can do and what cannot within your team. Roles have different permissions. How they work, please read related block of module.