const projectRepository = require("../repositories/project.repository"); const notificationService = require("../../notification/services/notification.service"); const { data, error } = require("../../../helpers/utils/wrapper"); const { BadRequestError, NotFoundError, ForbiddenError, ConflictError, } = require("../../../helpers/error"); const { slugify } = require("../../../helpers/utils/slug"); const { getProjectBackgroundColor } = require("../../../helpers/utils/projectTheme"); class ProjectService { async createProject(projectPayload, 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 create project"), ); } const { member_emails, ...projectData } = projectPayload; const admin = await projectRepository.findAdminById(actor.id); if (!admin) { return error(new NotFoundError("Admin not found")); } const normalizedEmails = this.normalizeEmails(member_emails || []); const users = normalizedEmails.length > 0 ? await projectRepository.findUsersByEmails(normalizedEmails) : []; if (normalizedEmails.length > 0) { const foundEmails = users.map((user) => user.email.toLowerCase()); const missingEmails = normalizedEmails.filter( (email) => !foundEmails.includes(email), ); if (missingEmails.length > 0) { return error( new NotFoundError( `User with these emails not found: ${missingEmails.join(", ")}`, ), ); } } const userIds = users.map((user) => user.id); if (userIds.length > 0) { const activeMemberships = await projectRepository.findActiveMembershipsByUserIds(userIds); if (activeMemberships.length > 0) { const assignedInterns = activeMemberships.map((membership) => { const email = membership.user?.email || "unknown email"; const projectName = membership.project?.project_name || "Unknown Project"; return `${email} already assigned to ${projectName}`; }); return error( new ConflictError( `One or more interns already have an active project: ${assignedInterns.join(", ")}` ) ); } } const projectSlug = await this.generateUniqueProjectSlug(projectData.project_name); const newProjectData = { id_admin: parseInt(actor.id), slug: projectSlug, project_icon: projectData.project_icon, background_color: getProjectBackgroundColor(projectData.project_icon), project_name: projectData.project_name, description: projectData.description || "", start_date: projectData.start_date, end_date: projectData.end_date, status: "active", }; const project = await projectRepository.createWithMembers( newProjectData, userIds, ); for (const idUser of userIds) { await notificationService.createNotificationForIntern( idUser, "Diterima ke Proyek", `Anda telah bergabung ke proyek '${project.project_name}'.` ).catch(err => console.error("Failed to send project assignment notification:", err)); } return data(this.mapProjectDetail(project)); } catch (err) { return error(err); } } async getAllProjects(query = {}, actor = {}) { try { const projects = await projectRepository.findAll({ status: query.status, actor, }); const mappedProjects = projects.map((project) => ({ id: project.id, slug: project.slug, project_icon: project.project_icon, background_color: project.background_color, project_name: project.project_name, description: project.description, start_date: project.start_date, end_date: project.end_date, status: project.status, admin: this.mapAdmin(project.admin), total_members: project.members?.length || 0, total_tasks: project.tasks?.length || 0, created_at: project.created_at, updated_at: project.updated_at, })); return data(mappedProjects); } catch (err) { return error(err); } } async getProjectById(id, actor = {}) { try { const project = await projectRepository.findByIdOrSlug(id); if (!project) { return error(new NotFoundError("Project not found")); } const accessError = await this.validateProjectAccess(project.id, actor); if (accessError) { return error(accessError); } return data(this.mapProjectDetail(project)); } catch (err) { return error(err); } } async updateProject(id, updatePayload, 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 update project"), ); } const existingProject = await projectRepository.findByIdOrSlug(id); if (!existingProject) { return error(new NotFoundError("Project not found")); } if (Object.keys(updatePayload).length === 0) { return error( new BadRequestError( "At least one field is required to update project", ), ); } const updateData = { ...updatePayload }; if (updateData.start_date || updateData.end_date) { const startDate = updateData.start_date || existingProject.start_date; const endDate = updateData.end_date || existingProject.end_date; if (new Date(endDate) < new Date(startDate)) { return error( new BadRequestError( "End date must be greater than or equal to start date", ), ); } } if (updateData.description === null) { updateData.description = ""; } if ( updateData.project_name !== undefined && updateData.project_name !== existingProject.project_name ) { updateData.slug = await this.generateUniqueProjectSlug( updateData.project_name, existingProject.id ); } if (updateData.project_icon !== undefined) { updateData.background_color = getProjectBackgroundColor(updateData.project_icon); } const updatedProject = await projectRepository.updateById(existingProject.id, updateData); return data(this.mapProjectDetail(updatedProject)); } catch (err) { return error(err); } } async archiveProject(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 archive project"), ); } const existingProject = await projectRepository.findByIdOrSlug(id); if (!existingProject) { return error(new NotFoundError("Project not found")); } if (existingProject.status === "archived") { return data(this.mapProjectDetail(existingProject)); } const archivedProject = await projectRepository.archiveById(existingProject.id); return data(this.mapProjectDetail(archivedProject)); } catch (err) { return error(err); } } async validateProjectAccess(idProject, actor = {}) { if (!actor?.id || !actor?.role) { return new ForbiddenError( "Access denied: authentication data is missing", ); } if (actor.role === "admin") { return null; } if (actor.role === "mentor") { const project = await projectRepository.findById(idProject); if (!project || project.id_admin !== parseInt(actor.id)) { return new ForbiddenError( "Access denied: you are not the mentor of this project", ); } return null; } if (actor.role === "intern") { const membership = await projectRepository.checkActiveMember( idProject, actor.id, ); if (!membership) { return new ForbiddenError( "Access denied: you are not a member of this project", ); } return null; } return new ForbiddenError("Access denied: invalid role"); } normalizeEmails(emails = []) { return [ ...new Set( emails.filter(Boolean).map((email) => email.trim().toLowerCase()), ), ]; } mapProjectDetail(project) { if (!project) return null; return { id: project.id, slug: project.slug, project_icon: project.project_icon, background_color: project.background_color, project_name: project.project_name, description: project.description, certificate_template: project.certificate_template, start_date: project.start_date, end_date: project.end_date, status: project.status, admin: this.mapAdmin(project.admin), members: project.members?.map((member) => this.mapProjectMember(member)) || [], tasks: project.tasks || [], total_members: project.members?.length || 0, total_tasks: project.tasks?.length || 0, created_at: project.created_at, updated_at: project.updated_at, }; } mapAdmin(admin) { if (!admin) return null; return { id: admin.id, full_name: `${admin.nama_depan}${admin.nama_belakang ? ` ${admin.nama_belakang}` : ""}`, email: admin.email, profile_picture: admin.profile_picture || null, professional_bio: admin.professional_bio || null, }; } mapProjectMember(member) { if (!member) return null; const user = member.user; const lowongan = user?.lamaran_magang?.lowongan_magang; return { id: member.id, id_user: member.id_user, status: member.status, user: { id: user.id, full_name: user.full_name, email: user.email, professional_bio: user.professional_bio || null, position: lowongan?.posisi || null, kelompok_peminatan: lowongan?.kelompok_peminatan || null, }, created_at: member.created_at, updated_at: member.updated_at, }; } async getMyProjects(actor = {}) { try { if (!actor?.id) { return error(new BadRequestError("User ID is required")); } if (actor.role !== "intern") { return error( new ForbiddenError("Access denied: only interns can view their projects"), ); } const projects = await projectRepository.findProjectsByIntern(actor.id); const mappedProjects = projects.map((project) => ({ id: project.id, slug: project.slug, project_icon: project.project_icon, background_color: project.background_color, project_name: project.project_name, description: project.description, start_date: project.start_date, end_date: project.end_date, status: project.status, admin: this.mapAdmin(project.admin), total_members: project.members?.length || 0, total_tasks: project.tasks?.length || 0, created_at: project.created_at, updated_at: project.updated_at, })); return data(mappedProjects); } catch (err) { return error(err); } } async getMyTasks(actor = {}) { try { if (!actor?.id) { return error(new BadRequestError("User ID is required")); } if (actor.role !== "intern") { return error( new ForbiddenError("Access denied: only interns can view their tasks"), ); } const tasks = await projectRepository.findTasksByIntern(actor.id); const mappedTasks = tasks.map((task) => { const submission = task.submissions?.[0] || null; return { id: task.id, slug: task.slug, id_project: task.id_project, project_slug: task.project?.slug || null, project_name: task.project?.project_name || null, project_icon: task.project?.project_icon || null, title: task.title, description: task.description, deadline_at: task.deadline_at, submission_type: task.submission_type, created_at: task.created_at, updated_at: task.updated_at, submission_status: submission ? "submitted" : "not_submitted", submission_details: submission ? { id: submission.id, file_path: submission.file_path, url_link: submission.url_link, submitted_at: submission.submitted_at, updated_at: submission.updated_at, } : null, }; }); return data(mappedTasks); } catch (err) { return error(err); } } async getMentorProjects(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 mentors/admins can view their created projects"), ); } const projects = await projectRepository.findProjectsByMentor(actor.id); const mappedProjects = projects.map((project) => ({ id: project.id, slug: project.slug, project_icon: project.project_icon, background_color: project.background_color, project_name: project.project_name, description: project.description, start_date: project.start_date, end_date: project.end_date, status: project.status, admin: this.mapAdmin(project.admin), total_members: project.members?.length || 0, total_tasks: project.tasks?.length || 0, created_at: project.created_at, updated_at: project.updated_at, })); return data(mappedProjects); } catch (err) { return error(err); } } async getInterns(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/mentor can access interns list"), ); } const interns = await projectRepository.findActiveInternsWithProjects(); const mappedInterns = interns.map((intern) => { const activeMembership = intern.project_members?.[0] || null; const projectName = activeMembership?.project?.project_name || "Unassigned"; const role = intern.lamaran_magang?.lowongan_magang?.posisi || "-"; return { id: intern.id, name: intern.full_name, email: intern.email, projectName: projectName, role: role, isAssignedByMentor: projectName !== "Unassigned", avatar: null }; }); return data(mappedInterns); } catch (err) { return error(err); } } async assignMember(payload, 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 mentors/admins can assign members"), ); } const { id_project, id_user } = payload; if (!id_project || !id_user) { return error(new BadRequestError("Project ID and User ID are required")); } const project = await projectRepository.findById(id_project); if (!project) { return error(new NotFoundError("Project not found")); } const user = await projectRepository.findUserByIdAndActive(id_user); if (!user) { return error(new NotFoundError("User not found or inactive")); } const existingMembership = await projectRepository.findMembership(id_project, id_user); let membership; if (existingMembership) { if (existingMembership.status === 'active') { return error(new ConflictError("User is already an active member of this project")); } membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active'); } else { membership = await projectRepository.assignMember(id_project, id_user); } await notificationService.createNotificationForIntern( id_user, "Diterima ke Proyek", `Anda telah bergabung ke proyek '${project.project_name}'.` ).catch(err => console.error("Failed to send project assignment notification:", err)); return data(membership); } catch (err) { return error(err); } } async generateUniqueProjectSlug(projectName, excludeProjectId = null) { const baseSlug = slugify(projectName, 'project'); let slug = baseSlug; let counter = 2; while (await projectRepository.findSlugExists(slug, excludeProjectId)) { slug = `${baseSlug}-${counter}`; counter += 1; } return slug; } } module.exports = new ProjectService();