feat: CRUD untuk feature project intern
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
const projectRepository = require("../repositories/project.repository");
|
||||
const { data, error } = require("../../../helpers/utils/wrapper");
|
||||
const {
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
ForbiddenError,
|
||||
} = require("../../../helpers/error");
|
||||
|
||||
class ProjectService {
|
||||
async createProject(projectPayload, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin 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 || []);
|
||||
|
||||
if (normalizedEmails.length > 8) {
|
||||
return error(
|
||||
new BadRequestError(
|
||||
"Project member limit exceeded. Maximum member is 8",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 newProjectData = {
|
||||
id_admin: parseInt(actor.id),
|
||||
project_icon: projectData.project_icon,
|
||||
project_name: projectData.project_name,
|
||||
description: projectData.description || "",
|
||||
start_date: projectData.start_date,
|
||||
end_date: projectData.end_date,
|
||||
max_members: 8,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
const userIds = users.map((user) => user.id);
|
||||
const project = await projectRepository.createWithMembers(
|
||||
newProjectData,
|
||||
userIds,
|
||||
);
|
||||
|
||||
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,
|
||||
project_icon: project.project_icon,
|
||||
project_name: project.project_name,
|
||||
description: project.description,
|
||||
start_date: project.start_date,
|
||||
end_date: project.end_date,
|
||||
max_members: project.max_members,
|
||||
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.findById(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") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin can update project"),
|
||||
);
|
||||
}
|
||||
|
||||
const existingProject = await projectRepository.findById(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 = "";
|
||||
}
|
||||
|
||||
const updatedProject = await projectRepository.updateById(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") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin can archive project"),
|
||||
);
|
||||
}
|
||||
|
||||
const existingProject = await projectRepository.findById(id);
|
||||
|
||||
if (!existingProject) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
if (existingProject.status === "archived") {
|
||||
return data(this.mapProjectDetail(existingProject));
|
||||
}
|
||||
|
||||
const archivedProject = await projectRepository.archiveById(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 === "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,
|
||||
project_icon: project.project_icon,
|
||||
project_name: project.project_name,
|
||||
description: project.description,
|
||||
start_date: project.start_date,
|
||||
end_date: project.end_date,
|
||||
max_members: project.max_members,
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProjectService();
|
||||
Reference in New Issue
Block a user