diff --git a/README.md b/README.md index 555052a..3b3677d 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,4 @@ base url: `http://localhost:3000` - Create a new project: `POST /api/organization/:organizationId/team/:teamId/project` - Update a project: `PUT /api/organization/:organizationId/team/:teamId/project/:projectId` +- Update the project status: `PATCH /api/organization/:organizationId/team/:teamId/project/:projectId/status` diff --git a/src/controllers/project.controller.js b/src/controllers/project.controller.js index fef5a81..6e7a1bd 100644 --- a/src/controllers/project.controller.js +++ b/src/controllers/project.controller.js @@ -502,3 +502,144 @@ export const updateProject = async (req, res, next) => { next(error); } }; + +/** + * @desc Update project status + * @route /api/organization/:organizationId/team/:teamId/project/:projectId/status + * @method PATCH + * @access private + */ +export const updateProjectStatus = async (req, res, next) => { + try { + const { organizationId, teamId, projectId } = req.params; + const { status } = req.body; + + // Validate required parameters + const paramsValidation = validateParams( + { organizationId, teamId, projectId }, + ['organizationId', 'teamId', 'projectId'], + ); + + if (!paramsValidation.success) { + return res.status(400).json({ + success: false, + message: paramsValidation.message, + }); + } + + // Validate status + if (!status) { + return res.status(400).json({ + success: false, + message: 'Status is required', + }); + } + + // Validate status value + const validStatuses = [ + 'PLANNING', + 'ACTIVE', + 'ON_HOLD', + 'COMPLETED', + 'CANCELED', + ]; + if (!validStatuses.includes(status)) { + return res.status(400).json({ + success: false, + message: `Status must be one of: ${validStatuses.join(', ')}`, + }); + } + + // Check if organization exists + const orgResult = await checkOrganization(organizationId); + if (!orgResult.success) { + return res.status(404).json({ + success: false, + message: orgResult.message, + }); + } + const existingOrg = orgResult.organization; + + // Check if team exists + const teamResult = await checkTeam(teamId, organizationId); + if (!teamResult.success) { + return res.status(404).json({ + success: false, + message: teamResult.message, + }); + } + const team = teamResult.team; + + // Check if project exists + const existingProject = await prisma.project.findFirst({ + where: { + id: projectId, + teamId: teamId, + organizationId: organizationId, + deletedAt: null, + }, + }); + + if (!existingProject) { + return res.status(404).json({ + success: false, + message: 'Project not found', + }); + } + + // Check permissions + const permissionCheck = checkTeamPermissions( + req.user, + existingOrg, + team, + 'update status of', + ); + + // Check if user is a project member with appropriate rights + const projectMembership = await prisma.projectMember.findFirst({ + where: { + projectId: projectId, + userId: req.user.id, + isActive: true, + role: { + in: ['PROJECT_OWNER', 'PROJECT_MANAGER'], + }, + }, + }); + + const hasPermission = permissionCheck.success || projectMembership !== null; + + if (!hasPermission) { + return res.status(403).json({ + success: false, + message: "You do not have permission to update this project's status", + }); + } + + // If status is the same, no need to update + if (existingProject.status === status) { + return res.status(200).json({ + success: true, + message: 'Project status remains unchanged', + data: existingProject, + }); + } + + // Update the project status + const updatedProject = await prisma.project.update({ + where: { id: projectId }, + data: { + status, + lastModifiedBy: req.user.id, + }, + }); + + res.status(200).json({ + success: true, + message: 'Project status updated successfully', + data: updatedProject, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/docs/swagger.json b/src/docs/swagger.json index b8e908f..acdd4af 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -3394,6 +3394,157 @@ } ] } + }, + "/api/organization/{organizationId}/team/{teamId}/project/{projectId}/status": { + "patch": { + "tags": ["project"], + "summary": "Update project status", + "description": "Updates the status of an existing project. Requires appropriate permissions (admin, organization owner, team manager, or project owner/manager).", + "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" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string", + "description": "New project status", + "enum": [ + "PLANNING", + "ACTIVE", + "ON_HOLD", + "COMPLETED", + "CANCELED" + ], + "example": "ACTIVE" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Project status updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Project status updated successfully" + }, + "data": { + "$ref": "#/components/schemas/ProjectBasic" + } + } + } + } + } + }, + "400": { + "description": "Bad request - Invalid status or missing status field", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Status is required or must be one of: PLANNING, ACTIVE, ON_HOLD, COMPLETED, CANCELED" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "You do not have permission to update this project's status" + } + } + } + } + } + }, + "404": { + "description": "Not found - Organization, team or project not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Project not found" + } + } + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, diff --git a/src/routes/project.routes.js b/src/routes/project.routes.js index d0daef5..67bd439 100644 --- a/src/routes/project.routes.js +++ b/src/routes/project.routes.js @@ -3,6 +3,7 @@ import { verifyAccessToken } from '../middlewares/auth.middleware.js'; import { createProject, updateProject, + updateProjectStatus, } from '../controllers/project.controller.js'; const router = Router(); @@ -19,4 +20,10 @@ router.put( updateProject, ); +router.patch( + '/api/organization/:organizationId/team/:teamId/project/:projectId/status', + verifyAccessToken, + updateProjectStatus, +); + export default router;