diff --git a/README.md b/README.md index af06b2f..8a1642a 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,9 @@ base url: `http://localhost:3000` - Update a project: `PUT /api/organization/:organizationId/team/:teamId/project/:projectId` - Update the project status: `PATCH /api/organization/:organizationId/team/:teamId/project/:projectId/status` - Update the project priority: `PATCH /api/organization/:organizationId/team/:teamId/project/:projectId/priority` -- Delete a project: `DELETE /api/organization/:organizationId/team/:teamId/project/:projectId` +- Delete a project: `DELETE /api/organization/:organizationId/team/:teamId/project/:projectId/delete` - Restore a project: `PATCH /api/organization/:organizationId/team/:teamId/project/:projectId/restore` - Add new member in the project: `POST /api/organization/:organizationId/team/:teamId/project/:projectId/addMember` - Remove member from a project: `DELETE /api/organization/:organizationId/team/:teamId/project/:projectId/removeMember` - Get all projects: `GET /api/organization/:organizationId/team/:teamId/project/all` +- Get a specific project: `GET /api/organization/:organizationId/team/:teamId/project/:projectId` diff --git a/src/controllers/project.controller.js b/src/controllers/project.controller.js index 263afa6..8a17baa 100644 --- a/src/controllers/project.controller.js +++ b/src/controllers/project.controller.js @@ -781,7 +781,7 @@ export const updateProjectPriority = async (req, res, next) => { /** * @desc Delete a project - * @route /api/organization/:organizationId/team/:teamId/project/:projectId + * @route /api/organization/:organizationId/team/:teamId/project/:projectId/delete * @method DELETE * @access private */ @@ -1291,3 +1291,129 @@ export const getAllProjects = async (req, res, next) => { next(error); } }; + +/** + * @desc Get a specific project + * @route /api/organization/:organizationId/team/:teamId/project/:projectId + * @method GET + * @access private + */ +export const getSpecificProject = async (req, res, next) => { + try { + const { organizationId, teamId, projectId } = req.params; + const user = req.user; + + // Validate required parameters + const validationResult = validateParams( + { organizationId, teamId, projectId }, + ['organizationId', 'teamId', 'projectId'], + ); + + if (!validationResult.success) { + return res.status(400).json({ message: validationResult.message }); + } + + // Check if organization exists + const orgResult = await checkOrganization(organizationId); + if (!orgResult.success) { + return res.status(404).json({ message: orgResult.message }); + } + + // Check if team exists + const teamResult = await checkTeam(teamId, organizationId); + if (!teamResult.success) { + return res.status(404).json({ message: teamResult.message }); + } + + // Check if project exists + const project = await prisma.project.findFirst({ + where: { + id: projectId, + teamId, + deletedAt: null, + }, + include: { + ProjectMember: { + where: { + leftAt: null, + }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + profilePic: true, + }, + }, + }, + orderBy: { + joinedAt: 'asc', + }, + }, + tasks: { + where: { + deletedAt: null, + }, + select: { + id: true, + title: true, + status: true, + priority: true, + dueDate: true, + }, + orderBy: { + createdAt: 'desc', + }, + }, + }, + }); + + if (!project) { + return res.status(404).json({ message: 'Project not found' }); + } + + // Check if user is a project member or has team permissions + const isMember = project.ProjectMember.some( + (member) => member.userId === user.id, + ); + const permissionResult = checkTeamPermissions( + user, + orgResult.organization, + teamResult.team, + 'view', + ); + + if (!isMember && !permissionResult.success) { + return res + .status(403) + .json({ message: 'You do not have permission to view this project' }); + } + + // Format the response data + const formattedProject = { + ...project, + members: project.ProjectMember.map((member) => ({ + id: member.id, + userId: member.userId, + role: member.role, + joinedAt: member.joinedAt, + isActive: member.isActive, + user: member.user, + })), + memberCount: project.ProjectMember.length, + taskCount: project.tasks.length, + userRole: + project.ProjectMember.find((member) => member.userId === user.id) + ?.role || null, + }; + + res.status(200).json({ + success: true, + data: formattedProject, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/docs/swagger.json b/src/docs/swagger.json index d3e4623..c0c6b99 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -3690,7 +3690,7 @@ ] } }, - "/api/organization/{organizationId}/team/{teamId}/project/{projectId}/": { + "/api/organization/{organizationId}/team/{teamId}/project/{projectId}/delete": { "delete": { "tags": ["Project"], "summary": "Delete a project", @@ -4100,7 +4100,7 @@ }, "/api/organization/{organizationId}/team/{teamId}/project/{projectId}/restore": { "patch": { - "tags": ["project"], + "tags": ["Project"], "summary": "Restore a deleted project", "description": "Restores a previously soft-deleted project. Requires admin, organization owner, or team manager permissions.", "parameters": [ @@ -4210,7 +4210,7 @@ }, "/api/organization/{organizationId}/team/{teamId}/project/all": { "get": { - "tags": ["project"], + "tags": ["Project"], "summary": "Get all projects for a team", "description": "Retrieves all projects for a specific team. Regular users only see projects they're members of, while admins/owners/managers see all projects.", "parameters": [ @@ -4311,6 +4311,116 @@ } ] } + }, + "/api/organization/{organizationId}/team/{teamId}/project/{projectId}/": { + "get": { + "tags": ["Project"], + "summary": "Get a specific project with details", + "description": "Retrieves detailed information about a specific project including members and tasks. Users must be project members or have team permissions to view.", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "description": "Organization ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "in": "path", + "description": "Team ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "description": "Project ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project details retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/ProjectDetails" + } + } + } + } + } + }, + "400": { + "description": "Bad request - Missing or invalid parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Project ID is required" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - User doesn't have permission to view this project", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "You do not have permission to view this project" + } + } + } + } + } + }, + "404": { + "description": "Not found - Organization, team or project not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Project not found" + } + } + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, @@ -5693,6 +5803,117 @@ ] } } + }, + "ProjectDetails": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string", + "enum": [ + "PLANNING", + "IN_PROGRESS", + "ON_HOLD", + "COMPLETED", + "CANCELLED" + ] + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "endDate": { + "type": "string", + "format": "date-time" + }, + "priority": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH", "URGENT"] + }, + "budget": { + "type": "number", + "nullable": true + }, + "teamId": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "progress": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectMember" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectTask" + } + }, + "memberCount": { + "type": "integer" + }, + "taskCount": { + "type": "integer" + }, + "userRole": { + "type": "string", + "nullable": true, + "enum": [ + "MEMBER", + "CONTRIBUTOR", + "REVIEWER", + "MANAGER", + "PROJECT_OWNER" + ] + } + } + }, + "ProjectTask": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "priority": { + "type": "string" + }, + "dueDate": { + "type": "string", + "format": "date-time", + "nullable": true + } + } } }, "securitySchemes": { diff --git a/src/routes/project.routes.js b/src/routes/project.routes.js index 04c3895..de73878 100644 --- a/src/routes/project.routes.js +++ b/src/routes/project.routes.js @@ -5,6 +5,7 @@ import { createProject, deleteProject, getAllProjects, + getSpecificProject, removeProjectMember, restoreProject, updateProject, @@ -39,7 +40,7 @@ router.patch( ); router.delete( - '/api/organization/:organizationId/team/:teamId/project/:projectId', + '/api/organization/:organizationId/team/:teamId/project/:projectId/delete', verifyAccessToken, deleteProject, ); @@ -68,4 +69,10 @@ router.get( getAllProjects, ); +router.get( + '/api/organization/:organizationId/team/:teamId/project/:projectId', + verifyAccessToken, + getSpecificProject, +); + export default router;