feat: implementasi slug di module task

This commit is contained in:
Muhammad Zhafran Ilham
2026-06-23 22:40:41 +07:00
parent b630ebbe3c
commit 857b92d956
4 changed files with 218 additions and 32 deletions
+71 -22
View File
@@ -11,6 +11,7 @@ const {
ForbiddenError,
ConflictError,
} = require('../../../helpers/error');
const { slugify } = require('../../../helpers/utils/slug');
class TaskService {
async createTask(idProject, payload, actor = {}) {
@@ -23,12 +24,14 @@ class TaskService {
return error(new ForbiddenError('Access denied: only admin or mentor can create task'));
}
const project = await taskRepository.findProjectById(idProject);
const project = await taskRepository.findProjectByIdOrSlug(idProject);
if (!project) {
return error(new NotFoundError('Project not found'));
}
const taskSlug = await this.generateUniqueTaskSlug(project.id, payload.title);
const accessError = await this.validateProjectAccess(idProject, actor);
if (accessError) {
@@ -39,6 +42,7 @@ class TaskService {
const task = await taskRepository.createTask({
id_project: parseInt(idProject),
slug: taskSlug,
title: payload.title,
description: payload.description,
deadline_at: deadlineAt,
@@ -46,7 +50,7 @@ class TaskService {
});
await notificationService.createNotificationForProjectInterns(
idProject,
project.id,
"Tugas Baru Ditambahkan",
`Mentor telah membuat tugas baru: '${task.title}' di proyek '${project.project_name}'.`
).catch(err => console.error("Failed to send task creation notifications:", err));
@@ -63,19 +67,19 @@ class TaskService {
return error(new BadRequestError('Project ID must be a valid number'));
}
const project = await taskRepository.findProjectById(idProject);
const project = await taskRepository.findProjectByIdOrSlug(idProject);
if (!project) {
return error(new NotFoundError('Project not found'));
}
const accessError = await this.validateProjectAccess(idProject, actor);
const accessError = await this.validateProjectAccess(project.id, actor);
if (accessError) {
return error(accessError);
}
const tasks = await taskRepository.findTasksByProject(idProject);
const tasks = await taskRepository.findTasksByProject(project.id);
const totalMembers = project.members?.length || 0;
const mappedTasks = tasks.map((task) => {
@@ -104,13 +108,13 @@ class TaskService {
}
}
async getTaskById(idTask, actor = {}) {
async getTaskById(idTask, actor = {}, projectIdentifier = null) {
try {
if (!this.isPositiveInteger(idTask)) {
return error(new BadRequestError('Task ID must be a valid number'));
if (!idTask) {
return error(new BadRequestError('Task ID or slug is required'));
}
const task = await taskRepository.findTaskById(idTask);
const task = await this.resolveTask(idTask, projectIdentifier);
if (!task) {
return error(new NotFoundError('Task not found'));
@@ -136,7 +140,7 @@ class TaskService {
}
}
async updateTask(idTask, payload, actor = {}) {
async updateTask(idTask, payload, actor = {}, projectIdentifier = null) {
try {
if (!this.isPositiveInteger(idTask)) {
return error(new BadRequestError('Task ID must be a valid number'));
@@ -150,7 +154,7 @@ class TaskService {
return error(new BadRequestError('At least one field is required to update task'));
}
const existingTask = await taskRepository.findTaskById(idTask);
const existingTask = await this.resolveTask(idTask, projectIdentifier);
if (!existingTask) {
return error(new NotFoundError('Task not found'));
@@ -162,7 +166,7 @@ class TaskService {
return error(accessError);
}
const submissionCount = await taskRepository.countSubmissionsByTask(idTask);
const submissionCount = await taskRepository.countSubmissionsByTask(existingTask.id);
if (
payload.submission_type &&
@@ -176,7 +180,17 @@ class TaskService {
const updateData = {};
if (payload.title !== undefined) updateData.title = payload.title;
if (payload.title !== undefined) {
updateData.title = payload.title;
if (payload.title !== existingTask.title) {
updateData.slug = await this.generateUniqueTaskSlug(
existingTask.id_project,
payload.title,
existingTask.id
);
}
}
if (payload.description !== undefined) updateData.description = payload.description;
if (payload.submission_type !== undefined) updateData.submission_type = payload.submission_type;
@@ -188,7 +202,7 @@ class TaskService {
);
}
const updatedTask = await taskRepository.updateTask(idTask, updateData);
const updatedTask = await taskRepository.updateTask(existingTask.id, updateData);
return data(this.mapTask(updatedTask));
} catch (err) {
@@ -196,7 +210,7 @@ class TaskService {
}
}
async deleteTask(idTask, actor = {}) {
async deleteTask(idTask, actor = {}, projectIdentifier = null) {
try {
if (!this.isPositiveInteger(idTask)) {
return error(new BadRequestError('Task ID must be a valid number'));
@@ -206,7 +220,7 @@ class TaskService {
return error(new ForbiddenError('Access denied: only admin or mentor can delete task'));
}
const task = await taskRepository.findTaskById(idTask);
const task = await this.resolveTask(idTask, projectIdentifier);
if (!task) {
return error(new NotFoundError('Task not found'));
@@ -218,13 +232,13 @@ class TaskService {
return error(accessError);
}
const submissionCount = await taskRepository.countSubmissionsByTask(idTask);
const submissionCount = await taskRepository.countSubmissionsByTask(task.id);
if (submissionCount > 0) {
return error(new ConflictError('Task already has submissions and cannot be deleted'));
}
await taskRepository.deleteTask(idTask);
await taskRepository.deleteTask(task.id);
return data(null);
} catch (err) {
@@ -232,7 +246,7 @@ class TaskService {
}
}
async submitTask(idTask, payload, file, actor = {}) {
async submitTask(idTask, payload, file, actor = {}, projectIdentifier = null) {
try {
if (!this.isPositiveInteger(idTask)) {
return error(new BadRequestError('Task ID must be a valid number'));
@@ -242,7 +256,7 @@ class TaskService {
return error(new ForbiddenError('Access denied: only intern can submit task'));
}
const task = await taskRepository.findTaskById(idTask);
const task = await this.resolveTask(idTask, projectIdentifier);
if (!task) {
return error(new NotFoundError('Task not found'));
@@ -255,7 +269,7 @@ class TaskService {
}
const submissionData = {
id_task: parseInt(idTask),
id_task: parseInt(task.id),
id_user: parseInt(actor.id),
};
@@ -285,7 +299,7 @@ class TaskService {
submissionData.url_link = payload.url_link;
}
const existingSubmission = await taskRepository.findSubmissionByTaskAndUser(idTask, actor.id);
const existingSubmission = await taskRepository.findSubmissionByTaskAndUser(task.id, actor.id);
let submission;
@@ -518,6 +532,7 @@ class TaskService {
return {
id: task.id,
id_project: task.id_project,
slug: task.slug,
title: task.title,
description: task.description,
deadline_at: task.deadline_at,
@@ -546,6 +561,7 @@ class TaskService {
...this.mapTask(task),
project: {
id: task.project?.id,
slug: task.project?.slug,
project_name: task.project?.project_name,
project_icon: task.project?.project_icon,
},
@@ -586,6 +602,7 @@ class TaskService {
...this.mapTask(task),
project: {
id: task.project?.id,
slug: task.project?.slug,
project_name: task.project?.project_name,
project_icon: task.project?.project_icon,
},
@@ -625,6 +642,38 @@ class TaskService {
const number = Number(value);
return Number.isInteger(number) && number > 0;
}
async generateUniqueTaskSlug(idProject, title, excludeTaskId = null) {
const baseSlug = slugify(title, 'task');
let slug = baseSlug;
let counter = 2;
while (await taskRepository.findTaskSlugExists(idProject, slug, excludeTaskId)) {
slug = `${baseSlug}-${counter}`;
counter += 1;
}
return slug;
}
async resolveTask(identifier, projectIdentifier = null) {
if (/^\d+$/.test(String(identifier))) {
return taskRepository.findTaskById(identifier);
}
if (!projectIdentifier) {
return null;
}
const project = await taskRepository.findProjectByIdOrSlug(projectIdentifier);
if (!project) {
return null;
}
return taskRepository.findTaskBySlug(project.id, identifier);
}
}
module.exports = new TaskService();