Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ base url: `http://localhost:3000`

- Create a new team in a specific organization: `POST /api/organization/:organizationId/department/:departmentId/team`
- Add new team members: `POST /api/organization/:organizationId/department/:departmentId/team/:teamId/addMember`
- Remove member from a team: `DELETE /api/organization/:organizationId/department/:departmentId/team/:teamId/members/:memberId`
- Update a team: `PUT /api/organization/:organizationId/department/:departmentId/team/:teamId`
- Upload team avatar: `POST /api/organization/:organizationId/department/:departmentId/team/:teamId/avatar/upload`
- Delete team avatar: `DELETE /api/organization/:organizationId/department/:departmentId/team/:teamId/avatar/delete`
- Delete a team: `DELETE /api/organization/:organizationId/department/:departmentId/team/:teamId`
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "team_members" ADD COLUMN "deletedAt" TIMESTAMP(3);
13 changes: 7 additions & 6 deletions prisma/schema/models/team.model.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ model Team {
}

model TeamMember {
id String @id @default(uuid()) @db.Uuid
teamId String @db.Uuid
userId String @db.Uuid
role TeamMemberRole
joinedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(uuid()) @db.Uuid
teamId String @db.Uuid
userId String @db.Uuid
role TeamMemberRole
joinedAt DateTime @default(now())
isActive Boolean @default(true)
deletedAt DateTime?

// Relations
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
Expand Down
177 changes: 177 additions & 0 deletions src/controllers/team.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,183 @@ export const addTeamMember = async (req, res, next) => {
}
};

/**
* @desc Remove member from a team (soft delete)
* @route /api/organization/:organizationId/department/:departmentId/team/:teamId/members/:memberId
* @method DELETE
* @access private - admins, organization owners, department managers, or team creators
*/
export const removeTeamMember = async (req, res, next) => {
try {
const { organizationId, departmentId, teamId, memberId } = req.params;

if (!organizationId || !departmentId || !teamId || !memberId) {
return res.status(400).json({
success: false,
message:
'Organization ID, Department ID, Team ID, and Member ID are required',
});
}

// Check if organization exists and is not deleted
const existingOrg = await prisma.organization.findFirst({
where: {
id: organizationId,
deletedAt: null,
},
include: {
owners: {
select: {
userId: true,
},
},
},
});

if (!existingOrg) {
return res.status(404).json({
success: false,
message: 'Organization not found',
});
}

// Check if department exists and is not deleted
const existingDep = await prisma.department.findFirst({
where: {
id: departmentId,
deletedAt: null,
},
});

if (!existingDep) {
return res.status(404).json({
success: false,
message: 'Department not found',
});
}

// Check if team exists and is not deleted
const team = await prisma.team.findFirst({
where: {
id: teamId,
organizationId,
departmentId,
deletedAt: null,
},
select: {
id: true,
name: true,
createdBy: true,
},
});

if (!team) {
return res.status(404).json({
success: false,
message: 'Team not found',
});
}

// Check if team member exists
const teamMember = await prisma.teamMember.findFirst({
where: {
id: memberId,
teamId,
},
include: {
user: {
select: {
id: true,
firstName: true,
lastName: true,
},
},
},
});

if (!teamMember) {
return res.status(404).json({
success: false,
message: 'Team member not found or already removed',
});
}

// Check permissions - admins, org owners, dep managers, or team creators can remove members
const isAdmin = req.user.role === 'ADMIN';
const isOwner = existingOrg.owners.some(
(owner) => owner.userId === req.user.id,
);
const isDepManager = existingDep.managerId === req.user.id;
const isTeamCreator = team.createdBy === req.user.id;

if (!isAdmin && !isOwner && !isDepManager && !isTeamCreator) {
return res.status(403).json({
success: false,
message: 'You do not have permission to remove members from this team',
});
}

// Prevent removing the team creator if they're the only leader
if (teamMember.userId === team.createdBy) {
// Check if there are other leaders in the team
const otherLeaders = await prisma.teamMember.findMany({
where: {
teamId,
role: 'LEADER',
userId: { not: team.createdBy },
},
});

if (otherLeaders.length === 0) {
return res.status(400).json({
success: false,
message:
'Cannot remove the only team leader. Please assign another leader first.',
});
}
}

// Soft delete the team member
const removedMember = await prisma.teamMember.update({
where: { id: memberId },
data: { deletedAt: new Date(), isActive: false },
include: {
user: {
select: {
firstName: true,
lastName: true,
},
},
},
});

return res.status(200).json({
success: true,
message: `Team member ${removedMember.user.firstName} ${removedMember.user.lastName} removed successfully`,
data: {
removedMember: {
id: removedMember.id,
userId: removedMember.userId,
name: `${removedMember.user.firstName} ${removedMember.user.lastName}`,
removedAt: removedMember.deletedAt,
},
team: {
id: team.id,
name: team.name,
},
},
});
} catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Team member not found',
});
}
Comment thread
mdawoud27 marked this conversation as resolved.
next(error);
}
};

/**
* @desc Update a team
* @route /api/organization/:organizationId/department/:departmentId/team/:teamId
Expand Down
Loading