feat(project): endpoint untuk complete sebuah project, update project biasa kini tanpa status
This commit is contained in:
@@ -163,6 +163,29 @@ class ProjectController {
|
||||
}
|
||||
}
|
||||
|
||||
async completeProject(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const actor = getActor(req);
|
||||
|
||||
const result = await projectService.completeProject(id, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Project completed successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyProjects(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
|
||||
@@ -80,11 +80,6 @@ const updateProjectModel = joi.object().keys({
|
||||
end_date: joi.date().optional().messages({
|
||||
'date.base': 'End date must be a valid date.',
|
||||
}),
|
||||
|
||||
status: joi.string().valid(...PROJECT_STATUS_VALUES).optional().messages({
|
||||
'string.base': 'Project status must be a string.',
|
||||
'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`,
|
||||
}),
|
||||
});
|
||||
|
||||
const getAllProjectsModel = joi.object().keys({
|
||||
|
||||
@@ -151,6 +151,18 @@ class ProjectRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async completeById(id) {
|
||||
return prisma.project.update({
|
||||
where: {
|
||||
id: parseInt(id),
|
||||
},
|
||||
data: {
|
||||
status: "completed",
|
||||
},
|
||||
include: this.projectDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async checkActiveMember(idProject, idUser) {
|
||||
return prisma.projectMember.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -259,6 +259,57 @@ class ProjectService {
|
||||
}
|
||||
}
|
||||
|
||||
async completeProject(id, actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin" && actor.role !== "mentor") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin or mentor can complete project"),
|
||||
);
|
||||
}
|
||||
|
||||
const existingProject = await projectRepository.findByIdOrSlug(id);
|
||||
|
||||
if (!existingProject) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
const accessError = await this.validateProjectAccess(existingProject.id, actor);
|
||||
if (accessError) {
|
||||
return error(accessError);
|
||||
}
|
||||
|
||||
if (existingProject.status === "completed") {
|
||||
return data(this.mapProjectDetail(existingProject));
|
||||
}
|
||||
|
||||
if (existingProject.status === "archived") {
|
||||
return error(new ConflictError("Archived project cannot be marked as completed"));
|
||||
}
|
||||
|
||||
if (existingProject.status !== "active") {
|
||||
return error(new ConflictError("Only active project can be marked as completed"));
|
||||
}
|
||||
|
||||
const completedProject = await projectRepository.completeById(existingProject.id);
|
||||
|
||||
await notificationService.createNotificationForProjectInterns(
|
||||
completedProject.id,
|
||||
"Proyek Selesai",
|
||||
`Proyek '${completedProject.project_name}' telah ditandai selesai.`
|
||||
).catch((err) =>
|
||||
console.error("Failed to send project completion notifications:", err)
|
||||
);
|
||||
|
||||
return data(this.mapProjectDetail(completedProject));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async validateProjectAccess(idProject, actor = {}) {
|
||||
if (!actor?.id || !actor?.role) {
|
||||
return new ForbiddenError(
|
||||
|
||||
@@ -101,10 +101,6 @@ const router = express.Router();
|
||||
* type: string
|
||||
* format: date
|
||||
* example: 2026-08-20
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, completed, archived]
|
||||
* example: completed
|
||||
* ProjectAdminSummary:
|
||||
* type: object
|
||||
* properties:
|
||||
@@ -730,6 +726,45 @@ router.patch('/update/:id', verifyJWT, isMentorOrAdmin, projectController.update
|
||||
*/
|
||||
router.delete('/delete/:id', verifyJWT, isMentorOrAdmin, projectController.archiveProject);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/complete/{id}:
|
||||
* patch:
|
||||
* summary: Mark project as completed
|
||||
* description: Mark an active project as completed. Completed projects are locked, so tasks and submissions can no longer be modified. The path parameter supports project ID or slug.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* oneOf:
|
||||
* - type: integer
|
||||
* - type: string
|
||||
* required: true
|
||||
* description: Project ID or slug
|
||||
* example: internify-platform-dev
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Project completed successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ProjectDetailResponse'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden
|
||||
* 404:
|
||||
* description: Project not found
|
||||
* 409:
|
||||
* description: Project cannot be completed
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.patch('/complete/:id', verifyJWT, isMentorOrAdmin, projectController.completeProject);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/my-projects:
|
||||
|
||||
Reference in New Issue
Block a user