diff --git a/README.md b/README.md index 3ecdd87..573ac70 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,5 @@ base url: `http://localhost:3000` - 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` +- Get all teams: `GET /api/organization/:organizationId/department/:departmentId/teams/all` +- Get a specific team: `GET /api/organization/:organizationId/department/:departmentId/teams/:teamId` diff --git a/src/controllers/team.controller.js b/src/controllers/team.controller.js index 6336585..4233af8 100644 --- a/src/controllers/team.controller.js +++ b/src/controllers/team.controller.js @@ -1040,7 +1040,7 @@ export const deleteTeam = async (req, res, next) => { /** * @desc Get all teams - * @route /api/organization/:organizationId/department/:departmentId/team/all + * @route /api/organization/:organizationId/department/:departmentId/teams/all * @method GET * @access private - admins, organization owners, department managers */ @@ -1193,3 +1193,198 @@ export const getAllTeams = async (req, res, next) => { next(error); } }; + +/** + * @desc Get specific team details + * @route /api/organization/:organizationId/department/:departmentId/teams/:teamId + * @method GET + * @access private - admins, organization owners, department managers, team members + */ +export const getSpecificTeam = 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, + }, + }); + + if (!existingDep) { + return res.status(404).json({ + success: false, + message: 'Department not found', + }); + } + + // Check if the user has access to view this team + const isAdmin = req.user.role === 'ADMIN'; + const isOwner = existingOrg.owners.some( + (owner) => owner.userId === req.user.id, + ); + const isDepManager = existingDep.managerId === req.user.id; + + // Check if the user is a member of this team + const isTeamMember = await prisma.teamMember.findFirst({ + where: { + teamId, + userId: req.user.id, + isActive: true, + }, + }); + + // Get the team details + const team = await prisma.team.findFirst({ + where: { + id: teamId, + organizationId, + departmentId, + deletedAt: null, + }, + include: { + creator: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + profilePic: true, + }, + }, + members: { + where: { + isActive: true, + }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + profilePic: true, + }, + }, + }, + }, + projects: { + where: { + deletedAt: null, + }, + select: { + id: true, + name: true, + description: true, + status: true, + }, + }, + reports: { + select: { + id: true, + name: true, + createdAt: true, + }, + orderBy: { + createdAt: 'desc', + }, + take: 5, + }, + department: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + if (!team) { + return res.status(404).json({ + success: false, + message: 'Team not found', + }); + } + + // If user is not admin, owner, department manager, or team member, deny access + if (!isAdmin && !isOwner && !isDepManager && !isTeamMember) { + return res.status(403).json({ + success: false, + message: 'You do not have permission to view this team', + }); + } + + // Calculate team statistics + const activeMembers = team.members.length; + const totalProjects = team.projects.length; + const projectsInProgress = team.projects.filter( + (project) => project.status === 'IN_PROGRESS', + ).length; + const completedProjects = team.projects.filter( + (project) => project.status === 'COMPLETED', + ).length; + + return res.status(200).json({ + success: true, + data: { + team: { + id: team.id, + name: team.name, + description: team.description, + avatar: team.avatar, + createdBy: team.createdBy, + createdAt: team.createdAt, + updatedAt: team.updatedAt, + creator: team.creator, + department: team.department, + }, + members: team.members.map((member) => ({ + id: member.id, + role: member.role, + user: member.user, + joinedAt: member.joinedAt, + })), + projects: team.projects, + recentReports: team.reports, + statistics: { + activeMembers, + totalProjects, + projectsInProgress, + completedProjects, + }, + }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 524d380..143f239 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -2569,7 +2569,7 @@ } } }, - "/api/organization/{organizationId}/department/{departmentId}/team/all": { + "/api/organization/{organizationId}/department/{departmentId}/teams/all": { "get": { "tags": ["Team"], "summary": "Get all teams in a department", @@ -2680,6 +2680,99 @@ } } } + }, + "/api/organization/{organizationId}/department/{departmentId}/teams/{teamId}": { + "get": { + "tags": ["Team"], + "summary": "Get specific team details", + "description": "Retrieve detailed information about a specific team including members, projects, and statistics. Requires admin privileges, organization ownership, department management, or team membership.", + "operationId": "getSpecificTeam", + "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", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Team details retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamDetailsResponse" + } + } + } + }, + "400": { + "description": "Bad request - missing parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "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" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } } }, "components": { @@ -4594,6 +4687,170 @@ "example": 3 } } + }, + "TeamDetailsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "team": { + "$ref": "#/components/schemas/TeamBasicInfo" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamMemberDetails" + } + }, + "projects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectBasicInfo" + } + }, + "recentReports": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReportBasicInfo" + } + }, + "statistics": { + "$ref": "#/components/schemas/TeamStatistics" + } + } + } + } + }, + "TeamBasicInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "avatar": { + "type": "string", + "format": "uri", + "nullable": true + }, + "createdBy": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "creator": { + "$ref": "#/components/schemas/UserBasicInfo" + }, + "department": { + "$ref": "#/components/schemas/DepartmentBasicInfo" + } + } + }, + "TeamMemberDetails": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "role": { + "type": "string", + "enum": ["LEADER", "MEMBER", "CONTRIBUTOR"] + }, + "user": { + "$ref": "#/components/schemas/UserBasicInfo" + }, + "joinedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "ProjectBasicInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "ON_HOLD"] + } + } + }, + "ReportBasicInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "TeamStatistics": { + "type": "object", + "properties": { + "activeMembers": { + "type": "integer", + "description": "Number of active team members" + }, + "totalProjects": { + "type": "integer", + "description": "Total number of projects" + }, + "projectsInProgress": { + "type": "integer", + "description": "Number of projects in progress" + }, + "completedProjects": { + "type": "integer", + "description": "Number of completed projects" + } + } + }, + "DepartmentBasicInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + } + } } } } diff --git a/src/routes/team.routes.js b/src/routes/team.routes.js index a98496f..5e4b60f 100644 --- a/src/routes/team.routes.js +++ b/src/routes/team.routes.js @@ -6,6 +6,7 @@ import { deleteTeam, deleteTeamAvatar, getAllTeams, + getSpecificTeam, removeTeamMember, updateTeam, uploadTeamAvatar, @@ -58,9 +59,15 @@ router.delete( ); router.get( - '/api/organization/:organizationId/department/:departmentId/team/all', + '/api/organization/:organizationId/department/:departmentId/teams/all', verifyAccessToken, getAllTeams, ); +router.get( + '/api/organization/:organizationId/department/:departmentId/teams/:teamId', + verifyAccessToken, + getSpecificTeam, +); + export default router;