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
Binary file not shown.
322 changes: 322 additions & 0 deletions src/controllers/sprint.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import prisma from '../config/prismaClient.js';
import { sprintvalidation } from '../validations/sprint.validation.js';

/**
* Validate required parameters
* @param {Object} params - Parameters to validate
* @param {Array} requiredParams - Required parameter names
* @returns {Object} - Validation result
*/
const validateParams = (params, requiredParams) => {
for (const param of requiredParams) {
if (!params[param]) {
return {
success: false,
message: `${param.charAt(0).toUpperCase() + param.slice(1)} is required`,
};
}
}
return { success: true };
};

/**
* Check if project exists and user has access
* @param {string} projectId - Project ID
* @param {string} organizationId - Organization ID
* @param {string} teamId - Team ID
* @param {Object} user - User object
* @returns {Promise<Object>} - Contains project, organization and permission info
*/
const checkProjectAccess = async (projectId, organizationId, teamId, user) => {
// Check if organization exists
const organization = await prisma.organization.findFirst({
where: {
id: organizationId,
deletedAt: null,
},
include: {
owners: {
select: {
userId: true,
},
},
},
});

if (!organization) {
return {
success: false,
message: 'Organization not found',
};
}

// Check if team exists
const team = await prisma.team.findFirst({
where: {
id: teamId,
organizationId,
deletedAt: null,
},
});

if (!team) {
return {
success: false,
message: 'Team not found',
};
}

// Check if project exists
const project = await prisma.project.findFirst({
where: {
id: projectId,
teamId,
organizationId,
deletedAt: null,
},
include: {
ProjectMember: {
where: {
userId: user.id,
leftAt: null,
},
select: {
role: true,
},
},
},
});

if (!project) {
return {
success: false,
message: 'Project not found',
};
}

// Check permissions
const isAdmin = user.role === 'ADMIN';
const isOrgOwner = organization.owners.some(
(owner) => owner.userId === user.id,
);
const isTeamManager = team.createdBy === user.id;
const isProjectOwner = project.ProjectMember.some(
(m) => m.role === 'PROJECT_OWNER',
);
const isProjectManager = project.ProjectMember.some(
(m) => m.role === 'PROJECT_MANAGER',
);

const hasPermission =
isAdmin ||
isOrgOwner ||
isTeamManager ||
isProjectOwner ||
isProjectManager;

return {
success: true,
project,
organization,
team,
hasPermission,
isAdmin,
isOrgOwner,
isTeamManager,
isProjectOwner,
isProjectManager,
};
};

/**
* Validate sprint dates
* @param {Date} startDate - Sprint start date
* @param {Date} endDate - Sprint end date
* @param {string} projectId - Project ID
* @returns {Promise<Object>} - Validation result
*/
const validateSprintDates = async (startDate, endDate, projectId) => {
// Basic date validation
if (new Date(startDate) >= new Date(endDate)) {
return {
success: false,
message: 'Start date must be before end date',
};
}

// Check for overlapping sprints
const overlappingSprint = await prisma.sprint.findFirst({
where: {
projectId,
OR: [
{
// New sprint starts during existing sprint
startDate: { lte: new Date(endDate) },
endDate: { gte: new Date(startDate) },
},
{
// New sprint encompasses existing sprint
startDate: { gte: new Date(startDate) },
endDate: { lte: new Date(endDate) },
},
],
},
});

if (overlappingSprint) {
return {
success: false,
message: 'Sprint dates overlap with existing sprint',
overlappingSprint,
};
}

return { success: true };
};

/**
* Determine sprint status based on dates
* @param {Date} startDate - Sprint start date
* @param {Date} endDate - Sprint end date
* @returns {string} - Sprint status
*/
const calculateSprintStatus = (startDate, endDate) => {
const now = new Date();
const start = new Date(startDate);
const end = new Date(endDate);

if (now < start) {
return 'PLANNING';
}
if (now >= start && now <= end) {
return 'ACTIVE';
}
return 'COMPLETED';
};

/**
* @desc Create a new sprint
* @route POST /api/organization/:organizationId/team/:teamId/project/:projectId/sprint
* @method POST
* @access private
*/
export const createSprint = async (req, res, next) => {
try {
const { organizationId, teamId, projectId } = req.params;
const user = req.user;

// 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,
});
}

// Check project access and permissions
const accessCheck = await checkProjectAccess(
projectId,
organizationId,
teamId,
user,
);
if (!accessCheck.success) {
return res
.status(accessCheck.message === 'Project not found' ? 404 : 403)
.json({
success: false,
message: accessCheck.message,
});
}

if (!accessCheck.hasPermission) {
return res.status(403).json({
success: false,
message: 'You do not have permission to create sprints in this project',
});
}

// Validate input
const { error } = sprintvalidation(req.body);
if (error) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: error.details.map((e) => e.message),
});
}

const { name, description, startDate, endDate, goal } = req.body;

// Validate sprint dates
const dateValidation = await validateSprintDates(
startDate,
endDate,
projectId,
);
if (!dateValidation.success) {
return res.status(400).json({
success: false,
message: dateValidation.message,
data: dateValidation.overlappingSprint
? { overlappingSprint: dateValidation.overlappingSprint }
: undefined,
});
}

// Calculate status based on dates
const status = calculateSprintStatus(startDate, endDate);

try {
// Get the highest current order value to place new sprint at the end
const lastSprint = await prisma.sprint.findFirst({
where: {
projectId,
},
orderBy: {
order: 'desc',
},
select: {
order: true,
},
});

const newOrder = lastSprint ? lastSprint.order + 1 : 0;

// Create the sprint
const sprint = await prisma.sprint.create({
data: {
name,
description,
startDate: new Date(startDate),
endDate: new Date(endDate),
status,
goal,
order: newOrder,
projectId,
},
});

res.status(201).json({
success: true,
message: 'Sprint created successfully',
data: sprint,
});
} catch (error) {
if (error.code === 'P2002') {
return res.status(400).json({
success: false,
message: 'A sprint with this name already exists in this project',
});
}
throw error;
}
} catch (error) {
next(error);
}
};
5 changes: 4 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import userRoutes from './routes/user.routes.js';
import orgRouter from './routes/organization.routes.js';
import teamRoutes from './routes/team.routes.js';
import projectRoutes from './routes/project.routes.js';
import sprintRoutes from './routes/sprint.routes.js';
import taskRoutes from './routes/task.routes.js';
import {
errorHandler,
Expand Down Expand Up @@ -55,7 +56,8 @@ app.use(passport.session());
// Cors Policy
app.use(
cors({
credentials: true, // allow cookies
origin: '*', // Allow all origins or specify allowed origins
Comment thread
Kimoo193 marked this conversation as resolved.
credentials: true, // Allow cookies
}),
);

Expand All @@ -80,6 +82,7 @@ app.use(userRoutes);
app.use(departmentRoutes);
app.use(teamRoutes);
app.use(projectRoutes);
app.use(sprintRoutes);
app.use(taskRoutes);

// Error handling middleware
Expand Down
13 changes: 13 additions & 0 deletions src/routes/sprint.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Router } from 'express';
import { createSprint } from '../controllers/sprint.controller.js';
import { verifyAccessToken } from '../middlewares/auth.middleware.js';

const router = Router();

router.post(
'/api/organization/:organizationId/team/:teamId/project/:projectId/sprint/create',
verifyAccessToken,
createSprint,
);

export default router;
Loading