feat: implementasi slug di module task
This commit is contained in:
@@ -85,7 +85,11 @@ class TaskController {
|
||||
async getTaskById(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await taskService.getTaskById(req.params.id, actor);
|
||||
const result = await taskService.getTaskById(
|
||||
req.params.id,
|
||||
actor,
|
||||
req.query.project || req.query.id_project
|
||||
);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
@@ -118,7 +122,12 @@ class TaskController {
|
||||
}
|
||||
|
||||
const actor = getActor(req);
|
||||
const result = await taskService.updateTask(req.params.id, validatePayload.data, actor);
|
||||
const result = await taskService.updateTask(
|
||||
req.params.id,
|
||||
validatePayload.data,
|
||||
actor,
|
||||
req.query.project || req.query.id_project
|
||||
);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
@@ -139,7 +148,11 @@ class TaskController {
|
||||
async deleteTask(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await taskService.deleteTask(req.params.id, actor);
|
||||
const result = await taskService.deleteTask(
|
||||
req.params.id,
|
||||
actor,
|
||||
req.query.project || req.query.id_project
|
||||
);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
@@ -178,7 +191,8 @@ class TaskController {
|
||||
req.params.id,
|
||||
validatePayload.data,
|
||||
req.file,
|
||||
actor
|
||||
actor,
|
||||
req.query.project || req.query.id_project
|
||||
);
|
||||
|
||||
if (result.err) {
|
||||
|
||||
@@ -59,6 +59,8 @@ class TaskRepository {
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
id_admin: true,
|
||||
slug: true,
|
||||
project_name: true,
|
||||
project_icon: true,
|
||||
members: {
|
||||
@@ -194,6 +196,100 @@ class TaskRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findProjectByIdOrSlug(identifier) {
|
||||
if (/^\d+$/.test(String(identifier))) {
|
||||
return this.findProjectById(identifier);
|
||||
}
|
||||
|
||||
return prisma.project.findUnique({
|
||||
where: {
|
||||
slug: identifier,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
status: 'active',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
id_user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findTaskBySlug(idProject, slug) {
|
||||
return prisma.task.findUnique({
|
||||
where: {
|
||||
id_project_slug: {
|
||||
id_project: parseInt(idProject),
|
||||
slug,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
id_admin: true,
|
||||
slug: true,
|
||||
project_name: true,
|
||||
project_icon: true,
|
||||
members: {
|
||||
where: {
|
||||
status: 'active',
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
full_name: true,
|
||||
email: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
submissions: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
full_name: true,
|
||||
email: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findTaskSlugExists(idProject, slug, excludeTaskId = null) {
|
||||
const where = {
|
||||
id_project: parseInt(idProject),
|
||||
slug,
|
||||
};
|
||||
|
||||
if (excludeTaskId) {
|
||||
where.id = {
|
||||
not: parseInt(excludeTaskId),
|
||||
};
|
||||
}
|
||||
|
||||
const task = await prisma.task.findFirst({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Boolean(task);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TaskRepository();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user