diff --git a/src/controllers/team.controller.js b/src/controllers/team.controller.js index 81a8ed2..3c1e4e9 100644 --- a/src/controllers/team.controller.js +++ b/src/controllers/team.controller.js @@ -752,3 +752,111 @@ export const deleteTeamAvatar = async (req, res, next) => { next(error); } }; + +/** + * @desc Delete team + * @route /api/organization/:organizationId/department/:departmentId/team/:teamId/ + * @method DELETE + * @access private - admins or organization owners only + */ +export const deleteTeam = async (req, res, next) => { + try { + const { organizationId, departmentId, teamId } = req.params; + + if (!organizationId || !departmentId || !teamId) { + return res.status(400).json({ + success: false, + message: 'Organization ID, Department ID, and Team 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, + }, + select: { managerId: true }, + }); + + 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, + deletedAt: null, + }, + select: { + id: true, + name: true, + description: true, + createdBy: true, + }, + }); + if (!team) { + return res.status(404).json({ + success: false, + message: 'Team not found', + }); + } + + // TODO: Extract all permission checks into a helper function like hasTeamAddPermission(user, org, dep, team) to simplify controller logic. + // Check permissions - only admins and organization owners + 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 isTeamManager = team.createdBy === req.user.id; + + if (!isAdmin && !isOwner && !isDepManager && !isTeamManager) { + return res.status(403).json({ + success: false, + message: + 'You do not have permission to delete this team in this department', + }); + } + + // delete the team + await prisma.team.update({ + where: { id: teamId }, + data: { deletedAt: new Date() }, + }); + + return res.status(200).json({ + success: true, + message: 'Team deleted successfully', + }); + } catch (error) { + next(error); + } +}; diff --git a/src/docs/swagger.json b/src/docs/swagger.json index e79c5fa..ac84a47 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -2290,6 +2290,137 @@ } } } + }, + "/api/organization/{organizationId}/department/{departmentId}/team/{teamId}": { + "delete": { + "tags": ["Team"], + "summary": "Delete a team", + "description": "Soft delete a team (marks as deleted but retains in database). Requires admin privileges, organization ownership, department management, or team leadership rights.", + "operationId": "deleteTeam", + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "description": "ID of the organization", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "departmentId", + "in": "path", + "required": true, + "description": "ID of the department", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "description": "ID of the team to delete", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Team deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Team deleted successfully" + } + } + } + } + } + }, + "400": { + "description": "Bad request - missing parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "missingParams": { + "value": { + "success": false, + "message": "Organization ID, Department ID, and Team ID are required" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Organization, department or team not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "orgNotFound": { + "value": { + "success": false, + "message": "Organization not found" + } + }, + "depNotFound": { + "value": { + "success": false, + "message": "Department not found" + } + }, + "teamNotFound": { + "value": { + "success": false, + "message": "Team not found" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } } }, "components": { diff --git a/src/routes/team.routes.js b/src/routes/team.routes.js index 79182c3..b3462fb 100644 --- a/src/routes/team.routes.js +++ b/src/routes/team.routes.js @@ -3,6 +3,7 @@ import { verifyAccessToken } from '../middlewares/auth.middleware.js'; import { addTeamMember, createTeam, + deleteTeam, deleteTeamAvatar, updateTeam, uploadTeamAvatar, @@ -42,4 +43,10 @@ router.delete( deleteTeamAvatar, ); +router.delete( + '/api/organization/:organizationId/department/:departmentId/team/:teamId/', + verifyAccessToken, + deleteTeam, +); + export default router;