From ab6ca1f7bfca2f3d2fe353d492b714ab3d5280bc Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Sat, 27 Jun 2026 23:40:24 +0700 Subject: [PATCH 1/2] docs: menghilangkan field task_done selain endpoint /get, /my-projects, dan /mentor-projects --- src/routes/project.routes.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/routes/project.routes.js b/src/routes/project.routes.js index 23335b0..f102036 100644 --- a/src/routes/project.routes.js +++ b/src/routes/project.routes.js @@ -125,7 +125,7 @@ const router = express.Router(); * type: string * nullable: true * example: Senior Program Manager at Internify. - * ProjectListItem: + * ProjectBaseItem: * type: object * properties: * id: @@ -136,6 +136,7 @@ const router = express.Router(); * example: internify-platform-dev * project_icon: * type: string + * enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield] * example: code * background_color: * type: string @@ -146,6 +147,10 @@ const router = express.Router(); * description: * type: string * example: Upgrading the core Internify platform backend and web client. + * certificate_template: + * type: string + * nullable: true + * example: /uploads/certificate-template.png * start_date: * type: string * format: date-time @@ -156,6 +161,7 @@ const router = express.Router(); * example: 2026-10-15T00:00:00.000Z * status: * type: string + * enum: [active, completed, archived] * example: active * admin: * $ref: '#/components/schemas/ProjectAdminSummary' @@ -165,10 +171,6 @@ const router = express.Router(); * total_tasks: * type: integer * example: 3 - * task_done: - * type: integer - * example: 2 - * description: Number of completed tasks. For interns, this counts tasks submitted by the logged-in intern. For admin/mentor, this counts tasks submitted by all active project members. * created_at: * type: string * format: date-time @@ -177,6 +179,15 @@ const router = express.Router(); * type: string * format: date-time * example: 2026-06-10T10:00:00.000Z + * ProjectListItem: + * allOf: + * - $ref: '#/components/schemas/ProjectBaseItem' + * - type: object + * properties: + * task_done: + * type: integer + * example: 2 + * description: Number of completed tasks. For interns, this counts tasks submitted by the logged-in intern. For admin/mentor, this counts tasks submitted by all active project members. * ProjectMemberUser: * type: object * properties: @@ -256,7 +267,7 @@ const router = express.Router(); * example: 2026-06-10T10:00:00.000Z * ProjectDetail: * allOf: - * - $ref: '#/components/schemas/ProjectListItem' + * - $ref: '#/components/schemas/ProjectBaseItem' * - type: object * properties: * members: From b82174a1aeb905af286189cc18e6eda234fa492f Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Sun, 28 Jun 2026 01:10:17 +0700 Subject: [PATCH 2/2] feat(project): implement filter multi status pada get all projects --- src/modules/project/models/project.model.js | 31 ++++++++++++-- .../repositories/project.repository.js | 8 ++-- .../project/services/project.service.js | 41 ++++++++++++++++++- src/routes/project.routes.js | 8 ++-- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/modules/project/models/project.model.js b/src/modules/project/models/project.model.js index 8eb9c40..b4f7410 100644 --- a/src/modules/project/models/project.model.js +++ b/src/modules/project/models/project.model.js @@ -88,10 +88,33 @@ const updateProjectModel = joi.object().keys({ }); const getAllProjectsModel = joi.object().keys({ - 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(', ')}.`, - }), + status: joi + .alternatives() + .try( + joi.string(), + joi.array().items(joi.string()) + ) + .optional() + .custom((value, helpers) => { + const statuses = Array.isArray(value) + ? value.flatMap((status) => String(status).split(',')) + : String(value).split(','); + + const invalidStatuses = statuses + .map((status) => status.trim().toLowerCase()) + .filter(Boolean) + .filter((status) => !PROJECT_STATUS_VALUES.includes(status)); + + if (invalidStatuses.length > 0) { + return helpers.error('any.only'); + } + + return value; + }) + .messages({ + 'alternatives.types': 'Project status must be a string or an array of strings.', + 'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`, + }), }); module.exports = { diff --git a/src/modules/project/repositories/project.repository.js b/src/modules/project/repositories/project.repository.js index c493fa7..43ebc2a 100644 --- a/src/modules/project/repositories/project.repository.js +++ b/src/modules/project/repositories/project.repository.js @@ -58,11 +58,13 @@ class ProjectRepository { }); } - async findAll({ status, actor }) { + async findAll({ statuses, actor }) { const where = {}; - if (status) { - where.status = status; + if (statuses && statuses.length > 0) { + where.status = { + in: statuses, + }; } if (actor?.role === "intern") { diff --git a/src/modules/project/services/project.service.js b/src/modules/project/services/project.service.js index 5758e40..1e21a5f 100644 --- a/src/modules/project/services/project.service.js +++ b/src/modules/project/services/project.service.js @@ -109,8 +109,13 @@ class ProjectService { async getAllProjects(query = {}, actor = {}) { try { + const statusFilter = this.parseProjectStatusFilter(query.status); + if (statusFilter.err) { + return statusFilter; + } + const projects = await projectRepository.findAll({ - status: query.status, + statuses: statusFilter.data, actor, }); @@ -293,6 +298,40 @@ class ProjectService { return new ForbiddenError("Access denied: invalid role"); } + parseProjectStatusFilter(rawStatus) { + if (!rawStatus) { + return data(null); + } + + const allowedStatuses = ["active", "completed", "archived"]; + + const statuses = Array.isArray(rawStatus) + ? rawStatus.flatMap((status) => String(status).split(",")) + : String(rawStatus).split(","); + + const cleanedStatuses = [ + ...new Set( + statuses + .map((status) => status.trim().toLowerCase()) + .filter(Boolean) + ), + ]; + + const invalidStatuses = cleanedStatuses.filter( + (status) => !allowedStatuses.includes(status) + ); + + if (invalidStatuses.length > 0) { + return error( + new BadRequestError( + `Invalid project status: ${invalidStatuses.join(", ")}` + ) + ); + } + + return data(cleanedStatuses); + } + normalizeEmails(emails = []) { return [ ...new Set( diff --git a/src/routes/project.routes.js b/src/routes/project.routes.js index f102036..f5ac657 100644 --- a/src/routes/project.routes.js +++ b/src/routes/project.routes.js @@ -534,10 +534,10 @@ router.post('/add', verifyJWT, isMentorOrAdmin, projectController.createProject) * name: status * schema: * type: string - * enum: [active, completed, archived] + * example: 'active,completed' * required: false - * description: Filter projects by status - * example: active + * description: Filter projects by one or more statuses. Use comma-separated values, for example active,completed. Values are **active, completed, archived** + * example: active,completed * responses: * 200: * description: Projects retrieved successfully @@ -545,6 +545,8 @@ router.post('/add', verifyJWT, isMentorOrAdmin, projectController.createProject) * application/json: * schema: * $ref: '#/components/schemas/ProjectListResponse' + * 400: + * description: Invalid project status * 401: * description: Unauthorized * 417: