feat: nambahin API assign member, list interns, update profile, serta list projects/tasks
This commit is contained in:
@@ -3,7 +3,9 @@ const { response, data, error } = require('../../../helpers/utils/wrapper');
|
||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||
const { BadRequestError, UnauthorizedError, InternalServerError } = require('../../../helpers/error');
|
||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||
const { loginModel } = require('../models/auth.model');
|
||||
const { loginModel, updateProfileModel } = require('../models/auth.model');
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
|
||||
|
||||
class AuthController {
|
||||
async login(req, res) {
|
||||
@@ -48,7 +50,7 @@ class AuthController {
|
||||
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
|
||||
}
|
||||
|
||||
const result = await authService.getCurrentUser(id);
|
||||
const result = await authService.getCurrentUser(id, req.role);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
@@ -67,6 +69,48 @@ class AuthController {
|
||||
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(req, res) {
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, updateProfileModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
if (req.file?.filename) {
|
||||
await deleteFileIfExists(req.file.filename).catch(() => { });
|
||||
}
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Data tidak valid',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const id = req.id;
|
||||
const role = req.role;
|
||||
|
||||
if (!id || !role) {
|
||||
if (req.file?.filename) {
|
||||
await deleteFileIfExists(req.file.filename).catch(() => { });
|
||||
}
|
||||
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
|
||||
}
|
||||
|
||||
const result = await authService.updateProfile(id, role, validatePayload.data, req.file);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Profil berhasil diperbarui', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
if (req.file?.filename) {
|
||||
await deleteFileIfExists(req.file.filename).catch(() => { });
|
||||
}
|
||||
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthController();
|
||||
|
||||
@@ -66,9 +66,28 @@ const resetPasswordModel = joi.object().keys({
|
||||
}),
|
||||
});
|
||||
|
||||
const updateProfileModel = joi.object().keys({
|
||||
full_name: joi.string().max(255).optional().messages({
|
||||
'string.base': 'Nama lengkap harus berupa teks.',
|
||||
'string.max': 'Nama lengkap tidak boleh lebih dari 255 karakter.'
|
||||
}),
|
||||
email: joi.string().email({ tlds: { allow: false } }).optional().messages({
|
||||
'string.base': 'Email harus berupa teks.',
|
||||
'string.email': 'Format email tidak valid.'
|
||||
}),
|
||||
password: joi.string().min(8).optional().messages({
|
||||
'string.base': 'Password harus berupa teks.',
|
||||
'string.min': 'Password minimal {#limit} karakter.'
|
||||
}),
|
||||
professional_bio: joi.string().allow('', null).optional().messages({
|
||||
'string.base': 'Bio harus berupa teks.'
|
||||
})
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
loginModel,
|
||||
registerModel,
|
||||
forgotPasswordModel,
|
||||
resetPasswordModel,
|
||||
updateProfileModel,
|
||||
};
|
||||
|
||||
@@ -2,58 +2,133 @@ const bcrypt = require('bcrypt');
|
||||
const adminService = require('../../admin/services/admin.service');
|
||||
const { createToken } = require('../../../middleware/verifyJWT');
|
||||
const { data, error } = require('../../../helpers/utils/wrapper');
|
||||
const { UnauthorizedError, NotFoundError } = require('../../../helpers/error');
|
||||
const { UnauthorizedError, NotFoundError, ForbiddenError, ConflictError, BadRequestError } = require('../../../helpers/error');
|
||||
const prisma = require('../../../helpers/db/db_connection');
|
||||
|
||||
class AuthService {
|
||||
async login(email, password) {
|
||||
try {
|
||||
const result = await adminService.getAdminByEmail(email);
|
||||
if (result.err || !result.data) {
|
||||
let userRecord = null;
|
||||
let isUserAdmin = false;
|
||||
|
||||
const adminResult = await adminService.getAdminByEmail(email);
|
||||
if (adminResult.data) {
|
||||
userRecord = adminResult.data;
|
||||
isUserAdmin = true;
|
||||
} else {
|
||||
const internRecord = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: email,
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
if (internRecord) {
|
||||
userRecord = internRecord;
|
||||
isUserAdmin = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userRecord) {
|
||||
return error(new UnauthorizedError('Email atau password tidak valid'));
|
||||
}
|
||||
|
||||
const admin = result.data;
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, admin.password);
|
||||
const isPasswordValid = await bcrypt.compare(password, userRecord.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return error(new UnauthorizedError('Email atau password tidak valid'));
|
||||
}
|
||||
|
||||
const token = createToken({
|
||||
id: admin.id.toString(),
|
||||
email: admin.email,
|
||||
role: admin.role,
|
||||
signature: admin.signature
|
||||
});
|
||||
const role = isUserAdmin ? (userRecord.role || 'admin') : 'intern';
|
||||
|
||||
return data({
|
||||
token,
|
||||
admin: {
|
||||
id: admin.id.toString(),
|
||||
nama_depan: admin.nama_depan,
|
||||
nama_belakang: admin.nama_belakang,
|
||||
email: admin.email,
|
||||
role: admin.role,
|
||||
signature: admin.signature,
|
||||
},
|
||||
});
|
||||
const tokenPayload = {
|
||||
id: userRecord.id.toString(),
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
signature: isUserAdmin ? userRecord.signature : null
|
||||
};
|
||||
|
||||
const token = createToken(tokenPayload);
|
||||
|
||||
if (isUserAdmin) {
|
||||
return data({
|
||||
token,
|
||||
user: {
|
||||
id: userRecord.id.toString(),
|
||||
full_name: `${userRecord.nama_depan}${userRecord.nama_belakang ? ' ' + userRecord.nama_belakang : ''}`,
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
signature: userRecord.signature
|
||||
},
|
||||
admin: {
|
||||
id: userRecord.id.toString(),
|
||||
nama_depan: userRecord.nama_depan,
|
||||
nama_belakang: userRecord.nama_belakang,
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
signature: userRecord.signature,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return data({
|
||||
token,
|
||||
user: {
|
||||
id: userRecord.id.toString(),
|
||||
full_name: userRecord.full_name,
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentUser(userId) {
|
||||
async getCurrentUser(userId, role) {
|
||||
try {
|
||||
const result = await adminService.getAdminById(userId);
|
||||
if (role === 'admin') {
|
||||
const result = await adminService.getAdminById(userId);
|
||||
|
||||
if (result.err) {
|
||||
return result;
|
||||
if (result.err) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { password, ...adminWithoutPassword } = result.data;
|
||||
|
||||
return data({
|
||||
...adminWithoutPassword,
|
||||
role: 'admin'
|
||||
});
|
||||
} else if (role === 'intern') {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: parseInt(userId),
|
||||
is_active: true
|
||||
},
|
||||
include: {
|
||||
lamaran_magang: {
|
||||
include: {
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
kelompok_peminatan: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return error(new NotFoundError('Pengguna tidak ditemukan'));
|
||||
}
|
||||
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
|
||||
return data(userWithoutPassword);
|
||||
} else {
|
||||
return error(new NotFoundError('Role tidak valid'));
|
||||
}
|
||||
|
||||
const { password, ...adminWithoutPassword } = result.data;
|
||||
|
||||
return data(adminWithoutPassword);
|
||||
} catch (err) {
|
||||
return error(new NotFoundError('Pengguna tidak ditemukan'));
|
||||
}
|
||||
@@ -67,6 +142,146 @@ class AuthService {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(userId, role, payload, imageFile) {
|
||||
try {
|
||||
const { full_name, email, password, professional_bio } = payload;
|
||||
|
||||
if (email) {
|
||||
const existingAdmin = await prisma.admin.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
if (existingAdmin && (role !== 'admin' || existingAdmin.id !== parseInt(userId))) {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new ConflictError('Email sudah terdaftar'));
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
if (existingUser && (role !== 'intern' || existingUser.id !== parseInt(userId))) {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new ConflictError('Email sudah terdaftar'));
|
||||
}
|
||||
}
|
||||
|
||||
let hashedPassword = undefined;
|
||||
if (password) {
|
||||
hashedPassword = await bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
if (role === 'admin') {
|
||||
const adminId = parseInt(userId);
|
||||
const existingAdmin = await prisma.admin.findUnique({
|
||||
where: { id: adminId }
|
||||
});
|
||||
if (!existingAdmin) {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new NotFoundError('Admin tidak ditemukan'));
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (full_name) {
|
||||
const nameParts = full_name.trim().split(/\s+/);
|
||||
updateData.nama_depan = nameParts[0];
|
||||
updateData.nama_belakang = nameParts.slice(1).join(' ') || '';
|
||||
}
|
||||
if (email) updateData.email = email;
|
||||
if (hashedPassword) updateData.password = hashedPassword;
|
||||
if (professional_bio !== undefined) updateData.professional_bio = professional_bio;
|
||||
|
||||
if (imageFile) {
|
||||
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
|
||||
const imagePath = imageUploadHelper.uploadImage(imageFile);
|
||||
updateData.profile_picture = imagePath;
|
||||
|
||||
if (existingAdmin.profile_picture) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
const path = require('path');
|
||||
const oldFileName = path.basename(existingAdmin.profile_picture);
|
||||
await deleteFileIfExists(oldFileName).catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedAdmin = await prisma.admin.update({
|
||||
where: { id: adminId },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
const { password: _, ...adminWithoutPassword } = updatedAdmin;
|
||||
return data({
|
||||
...adminWithoutPassword,
|
||||
role: 'admin'
|
||||
});
|
||||
|
||||
} else if (role === 'intern') {
|
||||
if (imageFile) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
return error(new ForbiddenError('Hanya admin yang dapat mengupload foto profil'));
|
||||
}
|
||||
|
||||
const internId = parseInt(userId);
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { id: internId }
|
||||
});
|
||||
if (!existingUser) {
|
||||
return error(new NotFoundError('User tidak ditemukan'));
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (full_name) updateData.full_name = full_name;
|
||||
if (email) updateData.email = email;
|
||||
if (hashedPassword) updateData.password = hashedPassword;
|
||||
if (professional_bio !== undefined) updateData.professional_bio = professional_bio;
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: internId },
|
||||
data: updateData,
|
||||
include: {
|
||||
lamaran_magang: {
|
||||
include: {
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
kelompok_peminatan: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { password: _, ...userWithoutPassword } = updatedUser;
|
||||
return data(userWithoutPassword);
|
||||
} else {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new NotFoundError('Role tidak valid'));
|
||||
}
|
||||
} catch (err) {
|
||||
if (imageFile?.filename) {
|
||||
try {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename);
|
||||
} catch (cleanupErr) {
|
||||
console.error('Failed to cleanup file:', cleanupErr);
|
||||
}
|
||||
}
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthService();
|
||||
|
||||
@@ -43,7 +43,7 @@ const sendConfirmationEmail = async (mahasiswa, lowongan) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendStatusEmail = async (lamaran, status) => {
|
||||
const sendStatusEmail = async (lamaran, status, generatedPassword = null) => {
|
||||
try {
|
||||
const { mahasiswa, lowongan_magang } = lamaran;
|
||||
const { nama_depan, nama_belakang, email } = mahasiswa;
|
||||
@@ -61,6 +61,14 @@ const sendStatusEmail = async (lamaran, status) => {
|
||||
<p>Halo ${fullName},</p>
|
||||
<p>Selamat! Lamaran Anda untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong> telah diterima.</p>
|
||||
<p>Tim kami sangat terkesan dengan profil dan kualifikasi Anda. Kami akan segera menghubungi Anda terkait tahapan selanjutnya.</p>
|
||||
${generatedPassword ? `
|
||||
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 5px; margin: 15px 0; border: 1px solid #e0e0e0; font-family: monospace;">
|
||||
<p style="margin-top: 0; font-weight: bold; color: #333; font-family: sans-serif;">Akun Portal Intern Anda telah dibuat:</p>
|
||||
<p style="margin: 5px 0;"><strong>Email:</strong> ${email}</p>
|
||||
<p style="margin: 5px 0;"><strong>Password:</strong> ${generatedPassword}</p>
|
||||
<p style="margin-bottom: 0; font-size: 13px; color: #555; font-family: sans-serif;">Silakan masuk menggunakan akun ini untuk melihat project dan tugas magang Anda.</p>
|
||||
</div>
|
||||
` : ''}
|
||||
<p>Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.</p>
|
||||
`
|
||||
: `
|
||||
|
||||
@@ -2,6 +2,9 @@ const lamaranMagangRepository = require('../repositories/lamaranMagang.repositor
|
||||
const mahasiswaRepository = require('../../mahasiswa/repositories/mahasiswa.repository');
|
||||
const lowonganMagangRepository = require('../../lowonganMagang/repositories/lowonganMagang.repository');
|
||||
const batchRepository = require('../../batch/repositories/batch.repository');
|
||||
const bcrypt = require('bcrypt');
|
||||
const prisma = require('../../../helpers/db/db_connection');
|
||||
const crypto = require('crypto');
|
||||
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
|
||||
const { sendConfirmationEmail, sendStatusEmail } = require('../helpers/email.helper');
|
||||
const ExcelJS = require('exceljs');
|
||||
@@ -151,7 +154,40 @@ class LamaranMagangService {
|
||||
|
||||
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status);
|
||||
|
||||
await sendStatusEmail(checkResult.data, status);
|
||||
let generatedPassword = null;
|
||||
if (status === 'diterima') {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id_mahasiswa: checkResult.data.id_mahasiswa },
|
||||
{ id_lamaran_magang: parseInt(id_lamaran_magang) },
|
||||
{ email: checkResult.data.mahasiswa.email }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
|
||||
if (!existingUser) {
|
||||
generatedPassword = crypto.randomBytes(4).toString('hex');
|
||||
const hashedPassword = await bcrypt.hash(generatedPassword, 10);
|
||||
|
||||
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? ' ' + checkResult.data.mahasiswa.nama_belakang : ''}`;
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id_mahasiswa: checkResult.data.id_mahasiswa,
|
||||
id_lamaran_magang: parseInt(id_lamaran_magang),
|
||||
full_name: fullName,
|
||||
email: checkResult.data.mahasiswa.email,
|
||||
password: hashedPassword,
|
||||
role: 'intern',
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await sendStatusEmail(checkResult.data, status, generatedPassword);
|
||||
|
||||
return data(result);
|
||||
} catch (err) {
|
||||
|
||||
@@ -162,6 +162,111 @@ class ProjectController {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyProjects(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getMyProjects(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Intern projects retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyTasks(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getMyTasks(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Intern tasks retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMentorProjects(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getMentorProjects(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Mentor projects retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getInterns(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getInterns(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Interns retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async assignMember(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.assignMember(req.body, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Member assigned successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProjectController();
|
||||
|
||||
@@ -203,6 +203,193 @@ class ProjectRepository {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async findProjectsByIntern(internId) {
|
||||
return prisma.project.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
id_user: parseInt(internId),
|
||||
status: "active",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
nama_depan: true,
|
||||
nama_belakang: true,
|
||||
email: true,
|
||||
profile_picture: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findTasksByIntern(internId) {
|
||||
return prisma.task.findMany({
|
||||
where: {
|
||||
project: {
|
||||
members: {
|
||||
some: {
|
||||
id_user: parseInt(internId),
|
||||
status: "active",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
deadline_at: "asc",
|
||||
},
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
project_icon: true,
|
||||
},
|
||||
},
|
||||
submissions: {
|
||||
where: {
|
||||
id_user: parseInt(internId),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
file_path: true,
|
||||
url_link: true,
|
||||
submitted_at: true,
|
||||
updated_at: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findProjectsByMentor(adminId) {
|
||||
return prisma.project.findMany({
|
||||
where: {
|
||||
id_admin: parseInt(adminId),
|
||||
},
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
nama_depan: true,
|
||||
nama_belakang: true,
|
||||
email: true,
|
||||
profile_picture: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findActiveInternsWithProjects() {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
role: "intern",
|
||||
is_active: true,
|
||||
},
|
||||
include: {
|
||||
project_members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lamaran_magang: {
|
||||
include: {
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async assignMember(id_project, id_user) {
|
||||
return prisma.projectMember.create({
|
||||
data: {
|
||||
id_project: parseInt(id_project),
|
||||
id_user: parseInt(id_user),
|
||||
status: "active",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findUserByIdAndActive(id) {
|
||||
return prisma.user.findFirst({
|
||||
where: {
|
||||
id: parseInt(id),
|
||||
is_active: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findMembership(idProject, idUser) {
|
||||
return prisma.projectMember.findFirst({
|
||||
where: {
|
||||
id_project: parseInt(idProject),
|
||||
id_user: parseInt(idUser),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateMembershipStatus(id, status) {
|
||||
return prisma.projectMember.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProjectRepository();
|
||||
|
||||
@@ -4,6 +4,7 @@ const {
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
ForbiddenError,
|
||||
ConflictError,
|
||||
} = require("../../../helpers/error");
|
||||
|
||||
class ProjectService {
|
||||
@@ -305,6 +306,209 @@ class ProjectService {
|
||||
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,
|
||||
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 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,
|
||||
id_project: task.id_project,
|
||||
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") {
|
||||
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,
|
||||
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 getInterns(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/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") {
|
||||
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"));
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project = await projectRepository.findById(id_project);
|
||||
if (!project) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
// Check if user exists and is active
|
||||
const user = await projectRepository.findUserByIdAndActive(id_user);
|
||||
if (!user) {
|
||||
return error(new NotFoundError("User not found or inactive"));
|
||||
}
|
||||
|
||||
// Check if already a member of this project
|
||||
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"));
|
||||
}
|
||||
// Reactivate membership
|
||||
membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active');
|
||||
} else {
|
||||
// Create new membership
|
||||
membership = await projectRepository.assignMember(id_project, id_user);
|
||||
}
|
||||
|
||||
return data(membership);
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProjectService();
|
||||
|
||||
Reference in New Issue
Block a user