refactor: rewrite ke codebase lama

This commit is contained in:
Muhammad Zhafran Ilham
2026-06-07 20:17:49 +07:00
parent ce5487dcbf
commit fe74b2213d
146 changed files with 14608 additions and 6686 deletions
+114
View File
@@ -0,0 +1,114 @@
const adminService = require('../services/admin.service');
const { response, data, error } = require('../../../helpers/utils/wrapper');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { ForbiddenError, BadRequestError, NotFoundError, InternalServerError } = require('../../../helpers/error');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createAdminModel, updateAdminModel } = require('../models/admin.model');
class AdminController {
async addAdmin(req, res) {
try {
const validatePayload = isValidPayload(req.body, createAdminModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await adminService.createAdmin(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', data(), 'Admin berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAdmins(req, res) {
try {
const result = await adminService.getAllAdmins();
if (result.err) {
return response(res, 'fail', result);
}
if (!result.data || result.data.length === 0) {
return response(res, 'fail', error(new NotFoundError('Tidak ada admin ditemukan')));
}
return response(res, 'success', result, 'Admin berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAdminById(req, res) {
try {
const { id } = req.params;
const result = await adminService.getAdminById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Admin berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateAdmin(req, res) {
try {
const { id } = req.params;
const validatePayload = isValidPayload(req.body, updateAdminModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await adminService.updateAdmin(id, validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', data(), 'Admin berhasil diperbarui', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteAdmin(req, res) {
try {
const { id } = req.params;
const result = await adminService.deleteAdmin(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', data(), 'Admin berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new AdminController();
+9
View File
@@ -0,0 +1,9 @@
const adminController = require('./controllers/admin.controller');
const adminService = require('./services/admin.service');
const adminRepository = require('./repositories/admin.repository');
module.exports = {
adminController,
adminService,
adminRepository
};
+75
View File
@@ -0,0 +1,75 @@
const joi = require('joi');
const createAdminModel = joi.object().keys({
nama_depan: joi.string().max(255).required().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.empty': 'Nama depan tidak boleh kosong.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
'any.required': 'Nama depan wajib diisi.',
}),
nama_belakang: joi.string().max(255).required().messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.empty': 'Nama belakang tidak boleh kosong.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
'any.required': 'Nama belakang wajib diisi.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
'any.required': 'Email wajib diisi.',
}),
password: joi.string().min(8).required().messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'any.required': 'Password wajib diisi.',
}),
role: joi.string().valid('admin').default('admin').messages({
'string.base': 'Role harus berupa teks.',
'any.only': 'Role hanya boleh "admin".',
}),
signature: joi.string().optional().allow('', null).messages({
'string.base': 'Signature harus berupa teks.',
}),
});
const updateAdminModel = joi.object().keys({
nama_depan: joi.string().max(255).optional().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.empty': 'Nama depan tidak boleh kosong.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
}),
nama_belakang: joi.string().max(255).optional().messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.empty': 'Nama belakang tidak boleh kosong.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).optional().messages({
'string.base': 'Email harus berupa teks.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
}),
password: joi.string().min(8).optional().messages({
'string.base': 'Password harus berupa teks.',
'string.min': 'Password minimal {#limit} karakter.',
}),
signature: joi.string().optional().allow('', null).messages({
'string.base': 'Signature harus berupa teks.',
}),
});
module.exports = {
createAdminModel,
updateAdminModel,
};
+40
View File
@@ -0,0 +1,40 @@
const prisma = require('../../../helpers/db/db_connection');
class AdminRepository {
async create(adminData) {
return prisma.admin.create({
data: adminData
});
}
async findAll() {
return prisma.admin.findMany();
}
async findByEmail(email) {
return prisma.admin.findUnique({
where: { email }
});
}
async findById(id) {
return prisma.admin.findUnique({
where: { id: parseInt(id) }
});
}
async updateById(id, adminData) {
return prisma.admin.update({
where: { id: parseInt(id) },
data: adminData
});
}
async deleteById(id) {
return prisma.admin.delete({
where: { id: parseInt(id) }
});
}
}
module.exports = new AdminRepository();
+114
View File
@@ -0,0 +1,114 @@
const bcrypt = require('bcrypt');
const adminRepository = require('../repositories/admin.repository');
const { data, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, NotFoundError } = require('../../../helpers/error');
class AdminService {
async createAdmin(adminData) {
try {
const { nama_depan, nama_belakang, email, password, role } = adminData;
const existingAdmin = await adminRepository.findByEmail(email);
if (existingAdmin) {
return error(new BadRequestError('Email sudah terdaftar'));
}
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
const newAdminData = {
nama_depan,
nama_belakang,
email,
password: hashedPassword,
role,
signature: `${nama_depan}-${Date.now()}`,
};
await adminRepository.create(newAdminData);
return data();
} catch (err) {
return error(err);
}
}
async getAllAdmins() {
try {
const admins = await adminRepository.findAll();
if (!admins || admins.length === 0) {
return error(new NotFoundError('Tidak ada admin ditemukan'));
}
return data(admins);
} catch (err) {
return error(err);
}
}
async getAdminById(id) {
try {
const admin = await adminRepository.findById(id);
if (!admin) {
return error(new NotFoundError('Admin tidak ditemukan'));
}
return data(admin);
} catch (err) {
return error(err);
}
}
async getAdminByEmail(email) {
try {
const admin = await adminRepository.findByEmail(email);
return data(admin);
} catch (err) {
return error(err);
}
}
async verifyPassword(plainPassword, hashedPassword) {
try {
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
return data(isValid);
} catch (err) {
return error(err);
}
}
async updateAdmin(id, adminData) {
try {
const adminResult = await this.getAdminById(id);
if (adminResult.err) {
return adminResult;
}
if (adminData.email) {
const existingAdmin = await adminRepository.findByEmail(adminData.email);
if (existingAdmin && existingAdmin.id !== id) {
return error(new BadRequestError('Email sudah terdaftar'));
}
}
await adminRepository.updateById(id, adminData);
return data();
} catch (err) {
return error(err);
}
}
async deleteAdmin(id) {
try {
const adminResult = await this.getAdminById(id);
if (adminResult.err) {
return adminResult;
}
await adminRepository.deleteById(id);
return data();
} catch (err) {
return error(err);
}
}
}
module.exports = new AdminService();
+72
View File
@@ -0,0 +1,72 @@
const authService = require('../services/auth.service');
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');
class AuthController {
async login(req, res) {
try {
const validatePayload = isValidPayload(req.body, loginModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { email, password } = validatePayload.data;
const result = await authService.login(email, password);
if (result.err) {
return response(res, 'fail', result);
}
console.log(result.data.token)
res.cookie("token", result.data.token, {
httpOnly: true,
secure: true,
sameSite: 'None',
maxAge: 7 * 24 * 60 * 60,
});
return response(res, 'success', result, 'Login berhasil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async me(req, res) {
try {
const id = req.id;
if (!id) {
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
}
const result = await authService.getCurrentUser(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Pengguna berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async logout(req, res) {
try {
return response(res, 'success', data(), 'Logout berhasil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new AuthController();
+7
View File
@@ -0,0 +1,7 @@
const authController = require('./controllers/auth.controller');
const authService = require('./services/auth.service');
module.exports = {
authController,
authService
};
+74
View File
@@ -0,0 +1,74 @@
const joi = require('joi');
const loginModel = joi.object().keys({
email: joi.string().email({ tlds: { allow: false } }).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'any.required': 'Email wajib diisi.',
}),
password: joi.string().required().messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'any.required': 'Password wajib diisi.',
}),
});
const registerModel = joi.object().keys({
email: joi.string().email({ tlds: { allow: false } }).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'any.required': 'Email wajib diisi.',
}),
password: joi.string()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.required()
.messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'string.pattern.base': 'Password harus mengandung huruf besar, huruf kecil, dan angka.',
'any.required': 'Password wajib diisi.',
}),
});
const forgotPasswordModel = joi.object().keys({
email: joi.string().email({ tlds: { allow: false } }).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'any.required': 'Email wajib diisi.',
}),
});
const resetPasswordModel = joi.object().keys({
token: joi.string().required().messages({
'string.base': 'Token harus berupa teks.',
'string.empty': 'Token tidak boleh kosong.',
'any.required': 'Token wajib diisi.',
}),
password: joi.string()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.required()
.messages({
'string.base': 'Password harus berupa teks.',
'string.empty': 'Password tidak boleh kosong.',
'string.min': 'Password minimal {#limit} karakter.',
'string.pattern.base': 'Password harus mengandung huruf besar, huruf kecil, dan angka.',
'any.required': 'Password wajib diisi.',
}),
});
module.exports = {
loginModel,
registerModel,
forgotPasswordModel,
resetPasswordModel,
};
+72
View File
@@ -0,0 +1,72 @@
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');
class AuthService {
async login(email, password) {
try {
const result = await adminService.getAdminByEmail(email);
if (result.err || !result.data) {
return error(new UnauthorizedError('Email atau password tidak valid'));
}
const admin = result.data;
const isPasswordValid = await bcrypt.compare(password, admin.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
});
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,
},
});
} catch (err) {
return error(err);
}
}
async getCurrentUser(userId) {
try {
const result = await adminService.getAdminById(userId);
if (result.err) {
return result;
}
const { password, ...adminWithoutPassword } = result.data;
return data(adminWithoutPassword);
} catch (err) {
return error(new NotFoundError('Pengguna tidak ditemukan'));
}
}
generateToken(payload) {
try {
const token = createToken(payload);
return data(token);
} catch (err) {
return error(err);
}
}
}
module.exports = new AuthService();
+130
View File
@@ -0,0 +1,130 @@
const batchService = require('../services/batch.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createBatchModel, updateBatchModel } = require('../models/batch.model');
class BatchController {
async createBatch(req, res) {
try {
const validatePayload = isValidPayload(req.body, createBatchModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await batchService.createBatch(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAllBatches(req, res) {
try {
const result = await batchService.getAllBatches();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil semua batch', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateBatch(req, res) {
try {
const id = parseInt(req.params.id);
const validatePayload = isValidPayload(req.body, updateBatchModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await batchService.updateBatch(id, validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil diperbarui', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteBatch(req, res) {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return response(
res,
'fail',
error(new BadRequestError('ID batch harus berupa angka')),
'ID batch tidak valid',
ERROR.BAD_REQUEST
);
}
const result = await batchService.deleteBatch(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async switchBatch(req, res) {
try {
const batch_number = parseInt(req.params.batch_number);
if (isNaN(batch_number)) {
return response(
res,
'fail',
error(new BadRequestError('Nomor batch harus berupa angka')),
'Nomor batch tidak valid',
ERROR.BAD_REQUEST
);
}
const result = await batchService.switchBatch(batch_number);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Batch berhasil diaktifkan', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new BatchController();
+9
View File
@@ -0,0 +1,9 @@
const batchController = require('./controllers/batch.controller');
const batchService = require('./services/batch.service');
const batchRepository = require('./repositories/batch.repository');
module.exports = {
batchController,
batchService,
batchRepository,
};
+30
View File
@@ -0,0 +1,30 @@
const joi = require('joi');
const createBatchModel = joi.object().keys({
batch_number: joi.number().integer().positive().messages({
'number.base': 'Nomor batch harus berupa angka.',
'number.integer': 'Nomor batch harus berupa bilangan bulat.',
'number.positive': 'Nomor batch harus berupa angka positif.',
}),
is_active: joi.boolean().default(true).messages({
'boolean.base': 'Status aktif harus berupa boolean.',
}),
});
const updateBatchModel = joi.object().keys({
batch_number: joi.number().integer().positive().messages({
'number.base': 'Nomor batch harus berupa angka.',
'number.integer': 'Nomor batch harus berupa bilangan bulat.',
'number.positive': 'Nomor batch harus berupa angka positif.',
}),
is_active: joi.boolean().messages({
'boolean.base': 'Status aktif harus berupa boolean.',
}),
});
module.exports = {
createBatchModel,
updateBatchModel,
};
+85
View File
@@ -0,0 +1,85 @@
const prisma = require('../../../helpers/db/db_connection');
class BatchRepository {
async create(batchData) {
return prisma.batch.create({
data: batchData,
});
}
async findAll() {
return prisma.batch.findMany({
orderBy: {
batch_number: 'desc',
},
});
}
async findById(id) {
return prisma.batch.findUnique({
where: { id: id },
});
}
async findByBatchNumber(batch_number) {
return prisma.batch.findUnique({
where: { batch_number: batch_number },
});
}
async findActiveBatch() {
return prisma.batch.findFirst({
where: { is_active: true },
});
}
async findLatestBatch() {
return prisma.batch.findFirst({
orderBy: {
batch_number: 'desc',
},
});
}
async updateById(id, batchData) {
return prisma.batch.update({
where: { id: id },
data: batchData,
});
}
async deleteById(id) {
return prisma.batch.delete({
where: { id: id },
});
}
async deactivateAllBatches() {
return prisma.batch.updateMany({
data: { is_active: false },
});
}
async countBatches() {
return prisma.batch.count();
}
async findByStatus(is_active) {
return prisma.batch.findMany({
where: { is_active: is_active },
orderBy: {
batch_number: 'desc',
},
});
}
async getBatchById(batchId) {
if (!batchId) {
return null;
}
return this.findById(parseInt(batchId));
}
}
module.exports = new BatchRepository();
+119
View File
@@ -0,0 +1,119 @@
const batchRepository = require('../repositories/batch.repository');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError, ConflictError } = require('../../../helpers/error');
class BatchService {
async createBatch(batchData) {
try {
let batchNumber;
if (batchData.batch_number) {
const existingBatch = await batchRepository.findByBatchNumber(batchData.batch_number);
if (existingBatch) {
return error(new ConflictError('Nomor batch sudah digunakan'));
}
batchNumber = batchData.batch_number;
} else {
const latestBatch = await batchRepository.findLatestBatch();
batchNumber = latestBatch ? latestBatch.batch_number + 1 : 1;
}
await batchRepository.deactivateAllBatches();
const newBatchData = {
...batchData,
batch_number: batchNumber,
is_active: true,
};
const result = await batchRepository.create(newBatchData);
return data(result);
} catch (err) {
return error(err);
}
}
async getAllBatches() {
try {
const batchList = await batchRepository.findAll();
return data(batchList);
} catch (err) {
return error(err);
}
}
async switchBatch(batch_number) {
try {
const batch = await batchRepository.findByBatchNumber(batch_number);
if (!batch) {
return error(new NotFoundError('Batch tidak ditemukan'));
}
await batchRepository.deactivateAllBatches();
const result = await batchRepository.updateById(batch.id, { is_active: true });
return data(result);
} catch (err) {
return error(err);
}
}
async getBatchById(id) {
try {
const batch = await batchRepository.findById(id);
if (!batch) {
return error(new NotFoundError('Batch tidak ditemukan'));
}
return data(batch);
} catch (err) {
return error(err);
}
}
async updateBatch(id, batchData) {
try {
const checkResult = await this.getBatchById(id);
if (checkResult.err) {
return checkResult;
}
console.log(batchData, id);
const existingBatch = checkResult.data;
if (batchData.batch_number && batchData.batch_number !== existingBatch.batch_number) {
const duplicateBatch = await batchRepository.findByBatchNumber(batchData.batch_number);
if (duplicateBatch) {
return error(new ConflictError('Nomor batch sudah digunakan'));
}
}
if (batchData.is_active === true) {
await batchRepository.deactivateAllBatches();
}
const result = await batchRepository.updateById(id, batchData);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteBatch(id) {
try {
const checkResult = await this.getBatchById(id);
if (checkResult.err) {
return checkResult;
}
const result = await batchRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new BatchService();
+173
View File
@@ -0,0 +1,173 @@
const faqService = require('../services/faq.services');
const { createFaqModel, updateFaqModel } = require('../models/faq.models');
const { response } = require('../../../helpers/utils/wrapper');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
class FaqController {
async addFaq(req, res) {
try {
const payload = req.body;
const validatePayload = isValidPayload(payload, createFaqModel);
if (validatePayload.err) {
return response(
res,
'fail',
validatePayload,
'Validasi gagal',
ERROR.EXPECTATION_FAILED
);
}
const result = await faqService.createFaq(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'FAQ berhasil ditambahkan',
SUCCESS.CREATED
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async getAllFaq(req, res) {
try {
const result = await faqService.getAllFaq();
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'Data FAQ berhasil diambil',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async getFaqById(req, res) {
try {
const { id } = req.params;
const result = await faqService.getFaqById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'Data FAQ berhasil diambil',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async updateFaq(req, res) {
try {
const { id } = req.params;
const payload = req.body;
const validatePayload = isValidPayload(payload, updateFaqModel);
if (validatePayload.err) {
return response(
res,
'fail',
validatePayload,
'Validasi gagal',
ERROR.EXPECTATION_FAILED
);
}
const result = await faqService.updateFaq(id, validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'FAQ berhasil diperbarui',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async deleteFaq(req, res) {
try {
const { id } = req.params;
const result = await faqService.deleteFaq(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(
res,
'success',
result,
'FAQ berhasil dihapus',
SUCCESS.OK
);
} catch (err) {
return response(
res,
'fail',
{ err: new InternalServerError(err.message) },
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
}
module.exports = new FaqController();
+5
View File
@@ -0,0 +1,5 @@
const faqController = require('./controllers/faq.controllers');
module.exports = {
faqController,
};
+26
View File
@@ -0,0 +1,26 @@
const joi = require('joi');
const createFaqModel = joi.object({
pertanyaan: joi.string().required().messages({
'string.base': 'Pertanyaan harus berupa teks.',
'any.required': 'Pertanyaan wajib diisi.',
}),
jawaban: joi.string().required().messages({
'string.base': 'Jawaban harus berupa teks.',
'any.required': 'Jawaban wajib diisi.',
}),
});
const updateFaqModel = joi.object({
pertanyaan: joi.string().messages({
'string.base': 'Pertanyaan harus berupa teks.',
}),
jawaban: joi.string().messages({
'string.base': 'Jawaban harus berupa teks.',
}),
});
module.exports = {
createFaqModel,
updateFaqModel,
};
+38
View File
@@ -0,0 +1,38 @@
const prisma = require('../../../helpers/db/db_connection');
class FaqRepository {
async create(data) {
return prisma.faq.create({
data: data,
});
}
async findAll() {
return prisma.faq.findMany({
orderBy: {
created_at: 'desc',
},
});
}
async findById(id) {
return prisma.faq.findUnique({
where: { id: parseInt(id) },
});
}
async updateById(id, data) {
return prisma.faq.update({
where: { id: parseInt(id) },
data: data,
});
}
async deleteById(id) {
return prisma.faq.delete({
where: { id: parseInt(id) },
});
}
}
module.exports = new FaqRepository();
+67
View File
@@ -0,0 +1,67 @@
const faqRepository = require('../repositories/faq.repositories');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError, BadRequestError } = require('../../../helpers/error');
class FaqService {
async createFaq(faqData) {
try {
const result = await faqRepository.create(faqData);
return data(result);
} catch (err) {
return error(err);
}
}
async getAllFaq() {
try {
const results = await faqRepository.findAll();
return data(results);
} catch (err) {
return error(err);
}
}
async getFaqById(id) {
try {
const result = await faqRepository.findById(id);
if (!result) {
return error(new NotFoundError('FAQ Tidak Ditemukan'));
}
return data(result);
} catch (err) {
return error(err);
}
}
async updateFaq(id, faqData) {
try {
const checkResult = await this.getFaqById(id);
if (checkResult.err) {
return checkResult;
}
const result = await faqRepository.updateById(id, faqData);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteFaq(id) {
try {
const checkResult = await this.getFaqById(id);
if (checkResult.err) {
return checkResult;
}
const result = await faqRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new FaqService();
+138
View File
@@ -0,0 +1,138 @@
const feedbackService = require('../services/feedback.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createFeedbackModel, updateFeedbackModel, getAllFeedbackModel } = require('../models/feedback.model');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
class FeedbackController {
async addFeedback(req, res) {
try {
const image_path = req.file;
const validatePayload = isValidPayload(req.body, createFeedbackModel);
if (validatePayload.err) {
if (image_path?.filename) {
await deleteFileIfExists(image_path.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await feedbackService.createFeedback(validatePayload.data, image_path);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Feedback berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAllFeedback(req, res) {
try {
const validatePayload = isValidPayload(req.query, getAllFeedbackModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Query parameter tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await feedbackService.getAllFeedback(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil semua feedback', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getFeedbackById(req, res) {
try {
const { id } = req.params;
const result = await feedbackService.getFeedbackById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil feedback berdasarkan ID', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateFeedback(req, res) {
try {
const { id } = req.params;
const image_path = req.file;
const validatePayload = isValidPayload(req.body, updateFeedbackModel);
if (validatePayload.err) {
if (image_path?.filename) {
await deleteFileIfExists(image_path.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await feedbackService.updateFeedback(id, validatePayload.data, image_path);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Feedback berhasil diperbarui', SUCCESS.OK);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteFeedback(req, res) {
try {
const { id } = req.params;
const result = await feedbackService.deleteFeedback(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Feedback berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new FeedbackController();
+5
View File
@@ -0,0 +1,5 @@
const feedbackController = require('./controllers/feedback.controller');
module.exports = {
feedbackController,
};
+108
View File
@@ -0,0 +1,108 @@
const joi = require('joi');
const createFeedbackModel = joi.object().keys({
nama: joi.string().max(255).required().messages({
'string.base': 'Nama harus berupa teks.',
'string.empty': 'Nama tidak boleh kosong.',
'string.max': 'Nama maksimal {#limit} karakter.',
'any.required': 'Nama wajib diisi.',
}),
universitas: joi.string().max(255).required().messages({
'string.base': 'Universitas harus berupa teks.',
'string.empty': 'Universitas tidak boleh kosong.',
'string.max': 'Universitas maksimal {#limit} karakter.',
'any.required': 'Universitas wajib diisi.',
}),
pesan: joi.string().required().messages({
'string.base': 'Pesan harus berupa teks.',
'string.empty': 'Pesan tidak boleh kosong.',
'any.required': 'Pesan wajib diisi.',
}),
batch: joi.number().integer().min(1).required().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
'any.required': 'Batch wajib diisi.',
}),
posisi: joi.string().max(255).required().messages({
'string.base': 'Posisi harus berupa teks.',
'string.empty': 'Posisi tidak boleh kosong.',
'string.max': 'Posisi maksimal {#limit} karakter.',
'any.required': 'Posisi wajib diisi.',
}),
tahun: joi.number().integer().min(2000).max(2100).required().messages({
'number.base': 'Tahun harus berupa angka.',
'number.integer': 'Tahun harus berupa bilangan bulat.',
'number.min': 'Tahun minimal {#limit}.',
'number.max': 'Tahun maksimal {#limit}.',
'any.required': 'Tahun wajib diisi.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const updateFeedbackModel = joi.object().keys({
nama: joi.string().max(255).optional().messages({
'string.base': 'Nama harus berupa teks.',
'string.max': 'Nama maksimal {#limit} karakter.',
}),
universitas: joi.string().max(255).optional().messages({
'string.base': 'Universitas harus berupa teks.',
'string.max': 'Universitas maksimal {#limit} karakter.',
}),
pesan: joi.string().optional().messages({
'string.base': 'Pesan harus berupa teks.',
}),
batch: joi.number().integer().min(1).optional().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
}),
posisi: joi.string().max(255).optional().messages({
'string.base': 'Posisi harus berupa teks.',
'string.max': 'Posisi maksimal {#limit} karakter.',
}),
tahun: joi.number().integer().min(2000).max(2100).optional().messages({
'number.base': 'Tahun harus berupa angka.',
'number.integer': 'Tahun harus berupa bilangan bulat.',
'number.min': 'Tahun minimal {#limit}.',
'number.max': 'Tahun maksimal {#limit}.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const getAllFeedbackModel = joi.object().keys({
batch: joi.number().integer().min(1).optional().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
}),
tahun: joi.number().integer().min(2000).max(2100).optional().messages({
'number.base': 'Tahun harus berupa angka.',
'number.integer': 'Tahun harus berupa bilangan bulat.',
'number.min': 'Tahun minimal {#limit}.',
'number.max': 'Tahun maksimal {#limit}.',
}),
});
module.exports = {
createFeedbackModel,
updateFeedbackModel,
getAllFeedbackModel,
};
+60
View File
@@ -0,0 +1,60 @@
const prisma = require('../../../helpers/db/db_connection');
class FeedbackRepository {
async create(feedbackData) {
return prisma.feedback.create({
data: {
...feedbackData,
created_at: new Date(),
},
});
}
async findAll() {
return prisma.feedback.findMany({
orderBy: {
created_at: 'desc',
},
});
}
async findAllWithFilters({ batch, tahun }) {
const where = {};
if (batch) {
where.batch = parseInt(batch);
}
if (tahun) {
where.tahun = parseInt(tahun);
}
return prisma.feedback.findMany({
where,
orderBy: {
created_at: 'desc',
},
});
}
async findById(id) {
return prisma.feedback.findUnique({
where: { id: parseInt(id) },
});
}
async updateById(id, feedbackData) {
return prisma.feedback.update({
where: { id: parseInt(id) },
data: feedbackData,
});
}
async deleteById(id) {
return prisma.feedback.delete({
where: { id: parseInt(id) },
});
}
}
module.exports = new FeedbackRepository();
+120
View File
@@ -0,0 +1,120 @@
const path = require('path');
const feedbackRepository = require('../repositories/feedback.repository');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError } = require('../../../helpers/error');
class FeedbackService {
async createFeedback(feedbackData, imageFile) {
try {
const imagePath = imageUploadHelper.uploadImage(imageFile);
const newFeedbackData = {
...feedbackData,
image_path: imagePath,
};
const result = await feedbackRepository.create(newFeedbackData);
return data(result);
} catch (err) {
if (imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup file:', cleanupErr);
}
}
return error(err);
}
}
async getAllFeedback(query = {}) {
try {
const feedbackList = await feedbackRepository.findAllWithFilters(query);
if (feedbackList.length === 0) {
return data([]);
}
return data(feedbackList);
} catch (err) {
return error(err);
}
}
async getFeedbackById(id) {
try {
const feedback = await feedbackRepository.findById(id);
if (!feedback) {
return error(new NotFoundError('Feedback tidak ditemukan'));
}
return data(feedback);
} catch (err) {
return error(err);
}
}
async updateFeedback(id, feedbackData, imageFile) {
let oldImagePath = null;
let newImageUploaded = false;
try {
const checkResult = await this.getFeedbackById(id);
if (checkResult.err) {
return checkResult;
}
const existingFeedback = checkResult.data;
if (imageFile) {
oldImagePath = existingFeedback.image_path;
feedbackData.image_path = imageUploadHelper.uploadImage(imageFile);
newImageUploaded = true;
if (oldImagePath) {
const oldFileName = path.basename(oldImagePath);
await deleteFileIfExists(oldFileName);
}
}
const result = await feedbackRepository.updateById(id, feedbackData);
return data(result);
} catch (err) {
if (newImageUploaded && imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup new file:', cleanupErr);
}
}
return error(err);
}
}
async deleteFeedback(id) {
try {
const checkResult = await this.getFeedbackById(id);
if (checkResult.err) {
return checkResult;
}
const feedback = checkResult.data;
if (feedback.image_path) {
const fileName = path.basename(feedback.image_path);
await deleteFileIfExists(fileName);
}
const result = await feedbackRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new FeedbackService();
@@ -0,0 +1,126 @@
const hasilResearchService = require('../services/hasilResearch.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createHasilResearchModel, updateHasilResearchModel } = require('../models/hasilResearch.model');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
class HasilResearchController {
async addHasilResearch(req, res) {
try {
const image = req.file;
const validatePayload = isValidPayload(req.body, createHasilResearchModel);
if (validatePayload.err) {
if (image?.filename) {
await deleteFileIfExists(image.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await hasilResearchService.createHasilResearch(validatePayload.data, image);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data hasil research berhasil ditambahkan', SUCCESS.CREATED);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getHasilResearch(req, res) {
try {
const result = await hasilResearchService.getAllHasilResearch();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data research berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getHasilResearchById(req, res) {
try {
const { id } = req.params;
const result = await hasilResearchService.getHasilResearchById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data research berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateHasilResearch(req, res) {
try {
const { id } = req.params;
const image = req.file;
const validatePayload = isValidPayload(req.body, updateHasilResearchModel);
if (validatePayload.err) {
if (image?.filename) {
await deleteFileIfExists(image.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await hasilResearchService.updateHasilResearch(id, validatePayload.data, image);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data hasil research berhasil diperbarui', SUCCESS.OK);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteHasilResearch(req, res) {
try {
const { id } = req.params;
const result = await hasilResearchService.deleteHasilResearch(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data hasil research berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new HasilResearchController();
+9
View File
@@ -0,0 +1,9 @@
const hasilResearchController = require('./controllers/hasilResearch.controller');
const hasilResearchService = require('./services/hasilResearch.service');
const hasilResearchRepository = require('./repositories/hasilResearch.repository');
module.exports = {
hasilResearchController,
hasilResearchService,
hasilResearchRepository
};
+52
View File
@@ -0,0 +1,52 @@
joi = require('joi');
const urlRegex = /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
const createHasilResearchModel = joi.object().keys({
nama_project: joi.string().max(255).required().messages({
'string.base': 'Nama project harus berupa teks.',
'string.empty': 'Nama project tidak boleh kosong.',
'string.max': 'Nama project maksimal {#limit} karakter.',
'any.required': 'Nama project wajib diisi.',
}),
deskripsi: joi.string().required().messages({
'string.base': 'Deskripsi harus berupa teks.',
'string.empty': 'Deskripsi tidak boleh kosong.',
'any.required': 'Deskripsi wajib diisi.',
}),
link_project: joi.string().pattern(urlRegex).optional().allow('', null).messages({
'string.base': 'Link project harus berupa teks.',
'string.pattern.base': 'Format URL tidak valid.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const updateHasilResearchModel = joi.object().keys({
nama_project: joi.string().max(255).optional().messages({
'string.base': 'Nama project harus berupa teks.',
'string.max': 'Nama project maksimal {#limit} karakter.',
}),
deskripsi: joi.string().optional().messages({
'string.base': 'Deskripsi harus berupa teks.',
}),
link_project: joi.string().pattern(urlRegex).optional().allow('', null).messages({
'string.base': 'Link project harus berupa teks.',
'string.pattern.base': 'Format URL tidak valid.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
module.exports = {
createHasilResearchModel,
updateHasilResearchModel,
};
@@ -0,0 +1,34 @@
const prisma = require('../../../helpers/db/db_connection');
class HasilResearchRepository {
async create(researchData) {
return prisma.hasilResearch.create({
data: researchData,
});
}
async findAll() {
return prisma.hasilResearch.findMany();
}
async findById(id) {
return prisma.hasilResearch.findUnique({
where: { id: parseInt(id) },
});
}
async updateById(id, researchData) {
return prisma.hasilResearch.update({
where: { id: parseInt(id) },
data: researchData,
});
}
async deleteById(id) {
return prisma.hasilResearch.delete({
where: { id: parseInt(id) },
});
}
}
module.exports = new HasilResearchRepository();
+121
View File
@@ -0,0 +1,121 @@
const hasilResearchRepository = require('../repositories/hasilResearch.repository');
const path = require('path');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError } = require('../../../helpers/error');
class HasilResearchService {
async createHasilResearch(researchData, imageFile) {
try {
const imagePath = imageUploadHelper.uploadImage(imageFile);
const newResearchData = {
...researchData,
image_path: imagePath,
};
const result = await hasilResearchRepository.create(newResearchData);
return data(result);
} catch (err) {
if (imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup file:', cleanupErr);
}
}
return error(err);
}
}
async getAllHasilResearch() {
try {
const researchList = await hasilResearchRepository.findAll();
return data(researchList);
} catch (err) {
return error(err);
}
}
async getHasilResearchById(id) {
try {
const research = await hasilResearchRepository.findById(id);
if (!research) {
return error(new NotFoundError('Hasil Research Tidak Ditemukan'));
}
return data(research);
} catch (err) {
return error(err);
}
}
async updateHasilResearch(id, researchData, imageFile) {
let oldImagePath = null;
let newImageUploaded = false;
try {
const checkResult = await this.getHasilResearchById(id);
if (checkResult.err) {
return checkResult;
}
const existingResearch = checkResult.data;
let imagePath = existingResearch.image_path;
if (imageFile) {
oldImagePath = existingResearch.image_path;
imagePath = imageUploadHelper.uploadImage(imageFile);
newImageUploaded = true;
if (oldImagePath) {
const oldFileName = path.basename(oldImagePath);
await deleteFileIfExists(oldFileName);
}
}
const updatedResearchData = {
...researchData,
image_path: imagePath,
};
const result = await hasilResearchRepository.updateById(id, updatedResearchData);
return data(result);
} catch (err) {
if (newImageUploaded && imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup new file:', cleanupErr);
}
}
return error(err);
}
}
async deleteHasilResearch(id) {
try {
const checkResult = await this.getHasilResearchById(id);
if (checkResult.err) {
return checkResult;
}
const research = checkResult.data;
if (research.image_path) {
const fileName = path.basename(research.image_path);
await deleteFileIfExists(fileName);
}
const result = await hasilResearchRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new HasilResearchService();
@@ -0,0 +1,312 @@
const lamaranMagangService = require('../services/lamaranMagang.service');
const { response, error, paginationResponse } = require('../../../helpers/utils/wrapper');
const { InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createLamaranMagangModel, updateLamaranStatusModel, getAllLamaranMagangModel, statisticModel } = require('../models/lamaranMagang.model');
const { cleanupUploadedFiles } = require('../helpers/cleanUpFiles');
class LamaranMagangController {
async addLamaranMagang(req, res) {
try {
const { id_lowongan_magang } = req.params;
const uploadedFiles = {
cv: req.files?.cv?.[0],
portofolio: req.files?.portofolio?.[0],
};
const payloadWithFiles = {
...req.body,
cv: uploadedFiles.cv?.filename,
portofolio: uploadedFiles.portofolio?.filename,
id_lowongan_magang,
};
const validatePayload = isValidPayload(payloadWithFiles, createLamaranMagangModel);
if (validatePayload.err) {
await cleanupUploadedFiles(uploadedFiles);
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { id_lowongan_magang: validatedLowonganId, cv, portofolio, ...mahasiswaFormData } = validatePayload.data;
const result = await lamaranMagangService.createLamaranMagang(
validatedLowonganId,
mahasiswaFormData,
uploadedFiles
);
if (result.err) {
await cleanupUploadedFiles(uploadedFiles);
return response(res, 'fail', result, 'Gagal mengajukan lamaran magang', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Lamaran magang berhasil diajukan', SUCCESS.CREATED);
} catch (err) {
await cleanupUploadedFiles({
cv: req.files?.cv?.[0],
portofolio: req.files?.portofolio?.[0],
});
return response(
res,
'fail',
error(new InternalServerError(err.message)),
'Terjadi kesalahan yang tidak terduga',
ERROR.INTERNAL_ERROR
);
}
}
async getAllLamaranMagang(req, res) {
try {
const validatePayload = isValidPayload(req.query, getAllLamaranMagangModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validatePayload.data;
const result = await lamaranMagangService.getAllLamaranMagang(query);
if (result.err) {
return response(res, 'fail', result);
}
return paginationResponse(res, 'success', result, 'Lamaran magang berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getLamaranByID(req, res) {
try {
const { id_lamaran_magang } = req.params;
const result = await lamaranMagangService.getLamaranById(id_lamaran_magang);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Lamaran magang berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getDetailLamaranById(req, res) {
try {
const { id } = req.params;
const result = await lamaranMagangService.getDetailLamaranById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Detail lamaran magang berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateStatusLamaran(req, res) {
try {
const { id_lamaran_magang } = req.params;
const validatePayload = isValidPayload(req.body, updateLamaranStatusModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { status } = validatePayload.data;
const result = await lamaranMagangService.updateLamaranStatus(id_lamaran_magang, status);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Status lamaran berhasil diperbarui', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteLamaran(req, res) {
try {
const { id } = req.params;
const result = await lamaranMagangService.deleteLamaran(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Lamaran magang berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async exportLamaranToExcel(req, res) {
try {
const result = await lamaranMagangService.exportToExcel();
if (result.err) {
return response(res, 'fail', result);
}
const workbook = result.data;
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader(
'Content-Disposition',
'attachment; filename=lamaran_magang.xlsx'
);
await workbook.xlsx.write(res);
res.end();
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getDashboardStatistics(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data
const result = await lamaranMagangService.getDashboardStatistics(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik dashboard', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik dashboard berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getStatisticsByPosition(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data;
const result = await lamaranMagangService.getStatisticsByPosition(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik berdasarkan posisi', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik berdasarkan posisi berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getStatisticsByCountry(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data;
const result = await lamaranMagangService.getStatisticsByCountry(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik berdasarkan negara', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik berdasarkan negara berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getStatisticsByUniversity(req, res) {
try {
const validateQuery = isValidPayload(req.query, statisticModel);
if (validateQuery.err) {
return response(
res,
'fail',
{ err: validateQuery.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validateQuery.data;
const result = await lamaranMagangService.getStatisticsByUniversity(query);
if (result.err) {
return response(res, 'fail', result, 'Gagal mengambil statistik berdasarkan universitas', ERROR.INTERNAL_ERROR);
}
return response(res, 'success', result, 'Statistik berdasarkan universitas berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new LamaranMagangController();
+14
View File
@@ -0,0 +1,14 @@
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const cleanupUploadedFiles = async (files) => {
if (files?.cv?.filename) {
await deleteFileIfExists(files.cv.filename);
}
if (files?.portofolio?.filename) {
await deleteFileIfExists(files.portofolio.filename);
}
};
module.exports = {
cleanupUploadedFiles,
};
+102
View File
@@ -0,0 +1,102 @@
const transporter = require('../../../helpers/utils/mail');
const sendConfirmationEmail = async (mahasiswa, lowongan) => {
try {
const { nama_depan, nama_belakang, email } = mahasiswa;
const { posisi } = lowongan;
const fullName = `${nama_depan}${nama_belakang ? ' ' + nama_belakang : ''}`;
const mailOptions = {
from: process.env.MAIL_USER,
to: email,
subject: 'Konfirmasi Penerimaan Lamaran Anda',
headers: {
'X-Priority': '1',
'X-MSMail-Priority': 'High',
Importance: 'high',
},
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
<h2 style="color: #333;">Lamaran Anda Telah Kami Terima</h2>
<p>Halo ${fullName},</p>
<p>Terima kasih telah mengirimkan lamaran untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong>.</p>
<p>Kami telah menerima dokumen dan informasi yang Anda kirimkan, dan saat ini tim rekrutmen kami sedang meninjaunya dengan seksama.</p>
<p>Jika profil Anda sesuai dengan kebutuhan kami, tim kami akan menghubungi Anda untuk tahapan seleksi berikutnya. Proses ini dapat memakan waktu hingga beberapa minggu.</p>
<p>Kami sangat menghargai minat Anda untuk bergabung bersama tim kami.</p>
<p>Salam hangat,</p>
<p><strong>Tim Rekrutmen Humic Engineering</strong></p>
<hr style="margin: 30px 0;">
<p style="font-size: 12px; color: #888;">Email ini dikirim secara otomatis. Jika Anda memiliki pertanyaan lebih lanjut, silakan hubungi kami melalui email resmi yang tersedia di website kami.</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
} catch (error) {
console.error('Error sending confirmation email:', error);
}
};
const sendStatusEmail = async (lamaran, status) => {
try {
const { mahasiswa, lowongan_magang } = lamaran;
const { nama_depan, nama_belakang, email } = mahasiswa;
const { posisi } = lowongan_magang;
const isAccepted = status === 'diterima';
const fullName = `${nama_depan}${nama_belakang ? ' ' + nama_belakang : ''}`;
const subject = isAccepted
? 'Selamat! Lamaran Anda Diterima'
: 'Pemberitahuan Status Lamaran Anda';
const message = isAccepted
? `
<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>
<p>Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.</p>
`
: `
<p>Halo ${fullName},</p>
<p>Terima kasih atas lamaran Anda untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong>.</p>
<p>Setelah mempertimbangkan secara seksama, kami memutuskan untuk tidak melanjutkan proses lamaran Anda ke tahap berikutnya.</p>
<p>Jangan berkecil hati—kami sangat menghargai waktu dan usaha Anda. Kami mendorong Anda untuk terus mencoba dan semoga sukses dalam perjalanan karier Anda.</p>
`;
const mailOptions = {
from: process.env.MAIL_USER,
to: email,
subject,
headers: {
'X-Priority': '1',
'X-MSMail-Priority': 'High',
Importance: 'high',
},
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
${message}
<p>Salam hangat,</p>
<p><strong>Tim Rekrutmen Humic Engineering</strong></p>
<hr style="margin: 30px 0;">
<p style="font-size: 12px; color: #888;">Email ini dikirim secara otomatis. Untuk informasi lebih lanjut, silakan hubungi kami melalui kontak resmi.</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
} catch (error) {
console.error('Gagal mengirim email status lamaran:', error);
}
};
module.exports = {
sendConfirmationEmail,
sendStatusEmail,
};
+5
View File
@@ -0,0 +1,5 @@
const lamaranMagangController = require('./controllers/lamaranMagang.controller');
module.exports = {
lamaranMagangController,
};
+206
View File
@@ -0,0 +1,206 @@
const joi = require('joi');
const phoneRegex = /^(\+62|62|0)8[1-9][0-9]{6,11}$/;
const createLamaranMagangModel = joi.object().keys({
id_lowongan_magang: joi.string().uuid().required().messages({
'string.base': 'ID lowongan magang harus berupa teks.',
'string.empty': 'ID lowongan magang tidak boleh kosong.',
'string.guid': 'Format ID lowongan magang tidak valid.',
'any.required': 'ID lowongan magang wajib diisi.',
}),
nama_depan: joi.string().max(255).required().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.empty': 'Nama depan tidak boleh kosong.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
'any.required': 'Nama depan wajib diisi.',
}),
nama_belakang: joi.string().max(255).optional().allow('', null).messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
'any.required': 'Email wajib diisi.',
}),
kontak: joi.string().pattern(phoneRegex).max(255).required().messages({
'string.base': 'Kontak harus berupa teks.',
'string.empty': 'Kontak tidak boleh kosong.',
'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).',
'string.max': 'Kontak maksimal {#limit} karakter.',
'any.required': 'Kontak wajib diisi.',
}),
jurusan: joi.string().max(255).required().messages({
'string.base': 'Jurusan harus berupa teks.',
'string.empty': 'Jurusan tidak boleh kosong.',
'string.max': 'Jurusan maksimal {#limit} karakter.',
'any.required': 'Jurusan wajib diisi.',
}),
universitas: joi.string().max(255).required().messages({
'string.base': 'Universitas harus berupa teks.',
'string.empty': 'Universitas tidak boleh kosong.',
'string.max': 'Universitas maksimal {#limit} karakter.',
'any.required': 'Universitas wajib diisi.',
}),
negara: joi.string().max(255).required().messages({
'string.base': 'Negara harus berupa teks.',
'string.empty': 'Negara tidak boleh kosong.',
'string.max': 'Negara maksimal {#limit} karakter.',
'any.required': 'Negara wajib diisi.',
}),
motivasi: joi.string().required().messages({
'string.base': 'Motivasi harus berupa teks.',
'string.empty': 'Motivasi tidak boleh kosong.',
'any.required': 'Motivasi wajib diisi.',
}),
relevant_skills: joi.string().max(255).required().messages({
'string.base': 'Keahlian yang relevan harus berupa teks.',
'string.empty': 'Keahlian yang relevan tidak boleh kosong.',
'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.',
'any.required': 'Keahlian yang relevan wajib diisi.',
}),
cv: joi.string().required().messages({
'string.base': 'Path CV harus berupa teks.',
'string.empty': 'Path CV tidak boleh kosong.',
'any.required': 'CV wajib diupload.',
}),
portofolio: joi.string().required().messages({
'string.base': 'Path portofolio harus berupa teks.',
'string.empty': 'Path portofolio tidak boleh kosong.',
'any.required': 'Portofolio wajib diupload.',
}),
status: joi.string().valid('diterima', 'diproses', 'ditolak').default('pending').messages({
'string.base': 'Status harus berupa teks.',
'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".',
}),
});
const updateLamaranStatusModel = joi.object().keys({
status: joi.string().valid('diterima', 'diproses', 'ditolak').required().messages({
'string.base': 'Status harus berupa teks.',
'string.empty': 'Status tidak boleh kosong.',
'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".',
'any.required': 'Status wajib diisi.',
}),
});
const updateLamaranMagangModel = joi.object().keys({
id_lowongan_magang: joi.string().uuid().optional().messages({
'string.base': 'ID lowongan magang harus berupa teks.',
'string.guid': 'Format ID lowongan magang tidak valid.',
}),
nama_depan: joi.string().max(255).optional().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
}),
nama_belakang: joi.string().max(255).optional().allow('', null).messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).optional().messages({
'string.base': 'Email harus berupa teks.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
}),
kontak: joi.string().pattern(phoneRegex).max(255).optional().messages({
'string.base': 'Kontak harus berupa teks.',
'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).',
'string.max': 'Kontak maksimal {#limit} karakter.',
}),
jurusan: joi.string().max(255).optional().messages({
'string.base': 'Jurusan harus berupa teks.',
'string.max': 'Jurusan maksimal {#limit} karakter.',
}),
universitas: joi.string().max(255).optional().messages({
'string.base': 'Universitas harus berupa teks.',
'string.max': 'Universitas maksimal {#limit} karakter.',
}),
negara: joi.string().max(255).optional().messages({
'string.base': 'Negara harus berupa teks.',
'string.max': 'Negara maksimal {#limit} karakter.',
}),
motivasi: joi.string().optional().messages({
'string.base': 'Motivasi harus berupa teks.',
}),
relevant_skills: joi.string().max(255).optional().messages({
'string.base': 'Keahlian yang relevan harus berupa teks.',
'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.',
}),
cv_path: joi.string().optional().messages({
'string.base': 'Path CV harus berupa teks.',
}),
portofolio_path: joi.string().optional().messages({
'string.base': 'Path portofolio harus berupa teks.',
}),
status: joi.string().valid('diterima', 'diproses', 'ditolak').optional().messages({
'string.base': 'Status harus berupa teks.',
'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".',
}),
});
const getAllLamaranMagangModel = joi.object().keys({
page: joi.number().integer().min(0).optional().messages({
'number.base': 'page harus berupa angka.',
}),
limit: joi.number().integer().min(1).optional().messages({
'number.base': 'Limit harus berupa angka.',
}),
posisi: joi.string().max(255).optional().messages({
'string.base': 'Posisi harus berupa teks.',
'string.max': 'Posisi maksimal {#limit} karakter.',
}),
status: joi.string().valid('diterima', 'diproses', 'ditolak').optional().messages({
'string.base': 'Status harus berupa teks.',
'any.only': 'Status hanya boleh "diterima", "diproses", atau "ditolak".',
}),
universitas: joi.string().max(255).optional().messages({
'string.base': 'Universitas harus berupa teks.',
'string.max': 'Universitas maksimal {#limit} karakter.',
}),
id_lowongan: joi.string().uuid().optional().messages({
'string.base': 'ID lowongan magang harus berupa teks.',
'string.guid': 'Format ID lowongan magang tidak valid.',
}),
});
const statisticModel = joi.object().keys({
batch: joi.number().integer().min(1).optional().messages({
'number.base': 'Batch harus berupa angka.',
'number.min': 'Batch minimal adalah {#limit}.',
}),
});
module.exports = {
createLamaranMagangModel,
updateLamaranStatusModel,
updateLamaranMagangModel,
getAllLamaranMagangModel,
statisticModel,
};
@@ -0,0 +1,292 @@
const prisma = require('../../../helpers/db/db_connection');
const { Prisma } = require('../../../generated/prisma')
class LamaranMagangRepository {
async create(lamaranData) {
return prisma.lamaranMagang.create({
data: {
...lamaranData,
created_at: new Date(),
update_at: new Date(),
},
});
}
async findAll({ offset, limit, status, posisi, universitas, id_lowongan }) {
const where = {};
if (status) {
where.status = status;
}
if (posisi) {
where.lowongan_magang = { posisi: posisi };
}
if (universitas) {
where.mahasiswa = { universitas: universitas };
}
if (id_lowongan) {
where.id_lowongan_magang = id_lowongan;
}
const lamaranList = await prisma.lamaranMagang.findMany({
skip: offset,
take: limit,
where,
include: {
mahasiswa: {
select: { nama_depan: true, nama_belakang: true },
},
lowongan_magang: {
select: { posisi: true, kelompok_peminatan: true },
},
batch_data: {
select: {
id: true,
batch_number: true,
is_active: true,
}
}
},
});
return lamaranList;
}
async countAllLamaran({ status, posisi, universitas }) {
const where = {};
if (status) {
where.status = status;
}
if (posisi) {
where.lowongan_magang = { posisi: posisi };
}
if (universitas) {
where.mahasiswa = { universitas: universitas };
}
return prisma.lamaranMagang.count(
{ where }
);
}
async findById(id_lamaran_magang) {
return prisma.lamaranMagang.findUnique({
where: { id: parseInt(id_lamaran_magang) },
include: {
mahasiswa: {
select: {
nama_depan: true,
nama_belakang: true,
email: true,
kontak: true,
jurusan: true,
cv_path: true,
portofolio_path: true,
motivasi: true,
relevant_skills: true,
universitas: true,
negara: true,
},
},
lowongan_magang: {
select: {
posisi: true,
},
},
batch_data: {
select: {
id: true,
batch_number: true,
is_active: true,
}
}
},
});
}
async findByMahasiswaAndLowongan(id_mahasiswa, id_lowongan_magang) {
return prisma.lamaranMagang.findFirst({
where: {
id_mahasiswa: parseInt(id_mahasiswa),
id_lowongan_magang: id_lowongan_magang,
},
});
}
async findAnyLamaranByMahasiswa(id_mahasiswa) {
return prisma.lamaranMagang.findFirst({
where: {
id_mahasiswa: parseInt(id_mahasiswa),
},
include: {
lowongan_magang: {
select: {
posisi: true,
},
},
},
});
}
async findDetailById(idLamaran) {
return prisma.lamaranMagang.findUnique({
where: { id: parseInt(idLamaran) },
include: {
mahasiswa: true,
lowongan_magang: true,
batch_data: {
select: {
id: true,
batch_number: true,
is_active: true,
}
}
},
});
}
async updateStatus(id_lamaran_magang, status) {
return prisma.lamaranMagang.update({
where: { id: parseInt(id_lamaran_magang) },
data: {
status,
update_at: new Date(),
},
});
}
async deleteById(id) {
return prisma.lamaranMagang.delete({
where: { id: parseInt(id) },
});
}
async getStatistics({ query }) {
let where = {};
if (query && query.batch) {
where.batch = parseInt(query.batch);
}
const [total, diterima, ditolak, diproses] = await Promise.all([
prisma.lamaranMagang.count({ where }),
prisma.lamaranMagang.count({ where: { ...where, status: 'diterima' } }),
prisma.lamaranMagang.count({ where: { ...where, status: 'ditolak' } }),
prisma.lamaranMagang.count({ where: { ...where, status: 'diproses' } }),
]);
return { total, diterima, ditolak, diproses };
}
async getStatisticsByPosition({ query }) {
let where = {};
if (query && query.batch) {
where.batch = parseInt(query.batch);
}
const data = await prisma.lamaranMagang.groupBy({
by: ['id_lowongan_magang'],
where,
_count: {
id: true,
},
});
const positions = await Promise.all(
data.map(async (item) => {
const lowongan = await prisma.lowonganMagang.findUnique({
where: { id: item.id_lowongan_magang },
select: { posisi: true },
});
const [total, diterima, ditolak, diproses] = await Promise.all([
prisma.lamaranMagang.count({
where: { id_lowongan_magang: item.id_lowongan_magang },
}),
prisma.lamaranMagang.count({
where: { id_lowongan_magang: item.id_lowongan_magang, status: 'diterima' },
}),
prisma.lamaranMagang.count({
where: { id_lowongan_magang: item.id_lowongan_magang, status: 'ditolak' },
}),
prisma.lamaranMagang.count({
where: { id_lowongan_magang: item.id_lowongan_magang, status: 'diproses' },
}),
]);
return {
posisi: lowongan?.posisi || 'Tidak Diketahui',
total,
diterima,
ditolak,
diproses,
};
})
);
return positions;
}
async getStatisticsByCountry({ query }) {
// Note : Menggunakan Raw Query karena Prisma ngga dukung join yang kompleks (lebih mudah juga :b)
const whereClause = query.batch
? Prisma.sql`WHERE lm.batch = ${parseInt(query.batch)}`
: Prisma.sql``;
const data = await prisma.$queryRaw`
SELECT
m.negara,
COUNT(*) as total,
SUM(lm.status = 'diterima') as diterima,
SUM(lm.status = 'ditolak') as ditolak,
SUM(lm.status = 'diproses') as diproses
FROM lamaran_magang lm
JOIN mahasiswa m ON lm.id_mahasiswa = m.id
${whereClause}
GROUP BY m.negara
ORDER BY total DESC
`;
return data.map((item) => ({
negara: item.negara,
total: Number(item.total),
diterima: Number(item.diterima),
ditolak: Number(item.ditolak),
diproses: Number(item.diproses),
}));
}
async getStatisticsByUniversity({ query }) {
const whereClause = query.batch
? Prisma.sql`WHERE lm.batch = ${parseInt(query.batch)}`
: Prisma.sql``;
const data = await prisma.$queryRaw`
SELECT
m.universitas,
COUNT(*) as total,
SUM(lm.status = 'diterima') as diterima,
SUM(lm.status = 'ditolak') as ditolak,
SUM(lm.status = 'diproses') as diproses
FROM lamaran_magang lm
JOIN mahasiswa m ON lm.id_mahasiswa = m.id
${whereClause}
GROUP BY m.universitas
ORDER BY total DESC
`;
return data.map((item) => ({
universitas: item.universitas,
total: Number(item.total),
diterima: Number(item.diterima),
ditolak: Number(item.ditolak),
diproses: Number(item.diproses),
}));
}
}
module.exports = new LamaranMagangRepository();
+311
View File
@@ -0,0 +1,311 @@
const lamaranMagangRepository = require('../repositories/lamaranMagang.repository');
const mahasiswaRepository = require('../../mahasiswa/repositories/mahasiswa.repository');
const lowonganMagangRepository = require('../../lowonganMagang/repositories/lowonganMagang.repository');
const batchRepository = require('../../batch/repositories/batch.repository');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const { sendConfirmationEmail, sendStatusEmail } = require('../helpers/email.helper');
const ExcelJS = require('exceljs');
const path = require('path');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const { data, error, paginationData } = require('../../../helpers/utils/wrapper');
const { NotFoundError, ConflictError } = require('../../../helpers/error');
class LamaranMagangService {
async createLamaranMagang(id_lowongan_magang, mahasiswaFormData, uploadedFiles) {
try {
const lowonganMagang = await lowonganMagangRepository.findById(id_lowongan_magang);
if (!lowonganMagang) {
return error(new NotFoundError('Lowongan Magang Tidak Ditemukan'));
}
const batchId = lowonganMagang.batch?.id || null;
const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaFormData.email);
if (existingMahasiswa) {
const existingLamaran = await lamaranMagangRepository.findAnyLamaranByMahasiswa(
existingMahasiswa.id
);
if (existingLamaran) {
const posisiYangSudahDilamar = existingLamaran.lowongan_magang?.posisi || 'posisi tertentu';
return error(new ConflictError(
`Anda sudah pernah melamar untuk posisi ${posisiYangSudahDilamar}. Setiap pengguna hanya dapat melamar satu posisi.`
));
}
const lamaranData = {
id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
};
await lamaranMagangRepository.create(lamaranData);
return data({
id_mahasiswa: existingMahasiswa.id,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
});
}
const cv_path = imageUploadHelper.uploadCV(uploadedFiles.cv);
const portofolio_path = imageUploadHelper.uploadPortofolio(uploadedFiles.portofolio);
const namaBelakangFinal = mahasiswaFormData.nama_belakang?.trim() || null;
const mahasiswaData = {
nama_depan: mahasiswaFormData.nama_depan,
nama_belakang: namaBelakangFinal,
email: mahasiswaFormData.email,
kontak: mahasiswaFormData.kontak,
jurusan: mahasiswaFormData.jurusan,
universitas: mahasiswaFormData.universitas,
negara: mahasiswaFormData.negara,
motivasi: mahasiswaFormData.motivasi,
relevant_skills: mahasiswaFormData.relevant_skills,
role: 'student',
cv_path,
portofolio_path,
};
const createdMahasiswa = await mahasiswaRepository.create(mahasiswaData);
const id_mahasiswa = createdMahasiswa.id;
const lamaranData = {
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
};
await lamaranMagangRepository.create(lamaranData);
await sendConfirmationEmail(mahasiswaData, lowonganMagang);
return data({
id_mahasiswa,
id_lowongan_magang,
batch: batchId,
status: 'diproses',
});
} catch (err) {
return error(err);
}
}
async getAllLamaranMagang(query = {}) {
try {
const { page = 1, limit = 10, posisi, status, universitas, id_lowongan } = query;
const offset = (page - 1) * limit;
const lamaranList = await lamaranMagangRepository.findAll({ offset, limit, status, posisi, universitas, id_lowongan });
const total = await lamaranMagangRepository.countAllLamaran({ status, posisi, universitas, id_lowongan });
const totalPages = Math.ceil(total / limit);
const meta = { page: Number(page), limit: Number(limit), total, totalPages };
return paginationData(lamaranList, meta);
} catch (err) {
return error(err);
}
}
async getLamaranById(id_lamaran_magang) {
try {
const lamaran = await lamaranMagangRepository.findById(id_lamaran_magang);
if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan'));
}
return data(lamaran);
} catch (err) {
return error(err);
}
}
async getDetailLamaranById(id_lamaran) {
try {
const lamaran = await lamaranMagangRepository.findDetailById(id_lamaran);
if (!lamaran) {
return error(new NotFoundError('Lamaran Magang Tidak Ditemukan'));
}
return data(lamaran);
} catch (err) {
return error(err);
}
}
async updateLamaranStatus(id_lamaran_magang, status) {
try {
const checkResult = await this.getDetailLamaranById(id_lamaran_magang);
if (checkResult.err) {
return checkResult;
}
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status);
await sendStatusEmail(checkResult.data, status);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteLamaran(id) {
try {
const checkResult = await this.getDetailLamaranById(id);
if (checkResult.err) {
return checkResult;
}
const lamaran = checkResult.data;
if (lamaran.cv_path) {
const cvFileName = path.basename(lamaran.cv_path);
await deleteFileIfExists(cvFileName);
}
if (lamaran.portofolio_path) {
const portofolioFileName = path.basename(lamaran.portofolio_path);
await deleteFileIfExists(portofolioFileName);
}
const result = await lamaranMagangRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
async exportToExcel() {
try {
const lamaranList = await lamaranMagangRepository.findAll({});
if (!lamaranList || lamaranList.length === 0) {
return error(new NotFoundError("Tidak ada data lamaran untuk diekspor"));
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Lamaran Magang');
worksheet.columns = [
{ header: 'ID', key: 'id', width: 10 },
{ header: 'Nama Depan', key: 'nama_depan', width: 20 },
{ header: 'Nama Belakang', key: 'nama_belakang', width: 20 },
{ header: 'Posisi', key: 'posisi', width: 25 },
{ header: 'Kelompok Peminatan', key: 'kelompok_peminatan', width: 25 },
{ header: 'Status', key: 'status', width: 15 },
{ header: 'Tanggal Dibuat', key: 'created_at', width: 20 },
];
lamaranList.forEach((lamaran) => {
worksheet.addRow({
id: lamaran.id,
nama_depan: lamaran.mahasiswa?.nama_depan || '',
nama_belakang: lamaran.mahasiswa?.nama_belakang || '',
posisi: lamaran.lowongan_magang?.posisi || '',
kelompok_peminatan: lamaran.lowongan_magang?.kelompok_peminatan || '',
status: lamaran.status,
created_at: lamaran.created_at,
});
});
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFD3D3D3' },
};
return data(workbook);
} catch (err) {
return error(err);
}
}
async getDashboardStatistics(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'Data Seluruh Batch';
const stats = await lamaranMagangRepository.getStatistics({ query });
const acceptanceRate = stats.total > 0 ? ((stats.diterima / stats.total) * 100).toFixed(1) : 0;
const rejectionRate = stats.total > 0 ? ((stats.ditolak / stats.total) * 100).toFixed(1) : 0;
const pendingRate = stats.total > 0 ? ((stats.diproses / stats.total) * 100).toFixed(1) : 0;
if (stats.total === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data({
total_pendaftar: stats.total,
total_diterima: stats.diterima,
total_ditolak: stats.ditolak,
sedang_diproses: stats.diproses,
presentase_diterima: `${acceptanceRate}%`,
presentase_ditolak: `${rejectionRate}%`,
presentase_diproses: `${pendingRate}%`,
batch: batchDisplay,
});
} catch (err) {
return error(err);
}
}
async getStatisticsByPosition(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch';
const positions = await lamaranMagangRepository.getStatisticsByPosition({ query });
if (positions.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data(positions);
} catch (err) {
return error(err);
}
}
async getStatisticsByCountry(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch';
const countries = await lamaranMagangRepository.getStatisticsByCountry({ query });
if (countries.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data(countries);
} catch (err) {
return error(err);
}
}
async getStatisticsByUniversity(query) {
try {
const batch = await batchRepository.getBatchById(query.batch);
const batchDisplay = batch ? batch.batch_number : 'semua batch';
const universities = await lamaranMagangRepository.getStatisticsByUniversity({ query });
if (universities.length === 0) {
return error(new NotFoundError(`Tidak ada data lamaran ditemukan pada batch ${batchDisplay}`));
}
return data(universities);
} catch (err) {
return error(err);
}
}
}
module.exports = new LamaranMagangService();
@@ -0,0 +1,157 @@
const lowonganMagangService = require('../services/lowonganMagang.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, ForbiddenError, InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createLowonganMagangModel, updateLowonganMagangModel } = require('../models/lowonganMagang.model');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
class LowonganMagangController {
async addLowonganMagang(req, res) {
try {
const image_path = req.file;
const validatePayload = isValidPayload(req.body, createLowonganMagangModel);
if (validatePayload.err) {
if (image_path?.filename) {
await deleteFileIfExists(image_path.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await lowonganMagangService.createLowonganMagang(validatePayload.data, image_path);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Lowongan magang berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAllLowonganMagang(req, res) {
try {
const result = await lowonganMagangService.getAllLowonganMagang();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil semua lowongan magang', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)),
'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getLowonganMagangById(req, res) {
try {
const { id } = req.params;
const result = await lowonganMagangService.getLowonganMagangById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil lowongan magang berdasarkan ID', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getLowonganByKelompokPeminatan(req, res) {
try {
const { kelompok_peminatan } = req.params;
const result = await lowonganMagangService.getLowonganByKelompokPeminatan(kelompok_peminatan);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil lowongan magang berdasarkan kelompok peminatan', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAllKelompokPeminatan(req, res) {
try {
const result = await lowonganMagangService.getAllKelompokPeminatan();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Berhasil mengambil semua kelompok peminatan', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateLowonganMagang(req, res) {
try {
const { id } = req.params;
const image_path = req.file;
const validatePayload = isValidPayload(req.body, updateLowonganMagangModel);
if (validatePayload.err) {
if (image_path?.filename) {
await deleteFileIfExists(image_path.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await lowonganMagangService.updateLowonganMagang(id, validatePayload.data, image_path);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Lowongan magang berhasil diperbarui', SUCCESS.OK);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteLowonganMagangById(req, res) {
try {
const { id } = req.params;
const result = await lowonganMagangService.deleteLowonganMagang(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Lowongan magang berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new LowonganMagangController();
+21
View File
@@ -0,0 +1,21 @@
class FileUploadHelper {
uploadImage(file) {
if (!file) {
throw new Error('Please upload an image!');
}
const filePath = `/uploads/${file.filename}`;
return filePath;
}
validateImage(file) {
if (!file) {
return false;
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'];
return allowedTypes.includes(file.mimetype);
}
}
module.exports = new FileUploadHelper();
+26
View File
@@ -0,0 +1,26 @@
class StatusHelper {
calculateStatus(durasi_awal, durasi_akhir) {
const now = new Date();
const startDate = new Date(durasi_awal);
const endDate = new Date(durasi_akhir);
if (now >= startDate && now <= endDate) {
return 'dibuka';
}
return 'ditutup';
}
updateLowonganStatus(lowongan) {
const status = this.calculateStatus(lowongan.durasi_awal, lowongan.durasi_akhir);
return {
...lowongan,
status_lowongan: status,
};
}
updateMultipleLowonganStatus(lowonganList) {
return lowonganList.map((lowongan) => this.updateLowonganStatus(lowongan));
}
}
module.exports = new StatusHelper();
+11
View File
@@ -0,0 +1,11 @@
const lowonganMagangController = require('./controllers/lowonganMagang.controller');
const lowonganMagangService = require('./services/lowonganMagang.service');
const lowonganMagangRepository = require('./repositories/lowonganMagang.repository');
const statusHelper = require('./helpers/status.helper');
module.exports = {
lowonganMagangController,
lowonganMagangService,
lowonganMagangRepository,
statusHelper
};
+128
View File
@@ -0,0 +1,128 @@
const joi = require('joi');
const createLowonganMagangModel = joi.object().keys({
posisi: joi.string().max(255).required().messages({
'string.base': 'Posisi harus berupa teks.',
'string.empty': 'Posisi tidak boleh kosong.',
'string.max': 'Posisi maksimal {#limit} karakter.',
'any.required': 'Posisi wajib diisi.',
}),
kelompok_peminatan: joi.string().max(255).required().messages({
'string.base': 'Kelompok peminatan harus berupa teks.',
'string.empty': 'Kelompok peminatan tidak boleh kosong.',
'string.max': 'Kelompok peminatan maksimal {#limit} karakter.',
'any.required': 'Kelompok peminatan wajib diisi.',
}),
jobdesk: joi.string().required().messages({
'string.base': 'Jobdesk harus berupa teks.',
'string.empty': 'Jobdesk tidak boleh kosong.',
'any.required': 'Jobdesk wajib diisi.',
}),
lokasi: joi.string().max(255).required().messages({
'string.base': 'Lokasi harus berupa teks.',
'string.empty': 'Lokasi tidak boleh kosong.',
'string.max': 'Lokasi maksimal {#limit} karakter.',
'any.required': 'Lokasi wajib diisi.',
}),
kualifikasi: joi.string().required().messages({
'string.base': 'Kualifikasi harus berupa teks.',
'string.empty': 'Kualifikasi tidak boleh kosong.',
'any.required': 'Kualifikasi wajib diisi.',
}),
benefit: joi.string().required().messages({
'string.base': 'Benefit harus berupa teks.',
'string.empty': 'Benefit tidak boleh kosong.',
'any.required': 'Benefit wajib diisi.',
}),
durasi_awal: joi.date().required().messages({
'date.base': 'Durasi awal harus berupa tanggal yang valid.',
'any.required': 'Durasi awal wajib diisi.',
}),
durasi_akhir: joi.date().min(joi.ref('durasi_awal')).required().messages({
'date.base': 'Durasi akhir harus berupa tanggal yang valid.',
'date.min': 'Durasi akhir harus setelah atau sama dengan durasi awal.',
'any.required': 'Durasi akhir wajib diisi.',
}),
paid: joi.string().valid('paid', 'unpaid').required().messages({
'string.empty': 'Status paid tidak boleh kosong.',
'string.base': 'Status paid harus berupa teks.',
'any.only': 'Status paid hanya boleh "paid" atau "unpaid".',
'any.required': 'Status paid wajib diisi.',
}),
batch: joi.number().integer().min(1).optional().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const updateLowonganMagangModel = joi.object().keys({
posisi: joi.string().max(255).optional().messages({
'string.base': 'Posisi harus berupa teks.',
'string.max': 'Posisi maksimal {#limit} karakter.',
}),
kelompok_peminatan: joi.string().max(255).optional().messages({
'string.base': 'Kelompok peminatan harus berupa teks.',
'string.max': 'Kelompok peminatan maksimal {#limit} karakter.',
}),
jobdesk: joi.string().optional().messages({
'string.base': 'Jobdesk harus berupa teks.',
}),
lokasi: joi.string().max(255).optional().messages({
'string.base': 'Lokasi harus berupa teks.',
'string.max': 'Lokasi maksimal {#limit} karakter.',
}),
kualifikasi: joi.string().optional().messages({
'string.base': 'Kualifikasi harus berupa teks.',
}),
benefit: joi.string().optional().messages({
'string.base': 'Benefit harus berupa teks.',
}),
durasi_awal: joi.date().optional().messages({
'date.base': 'Durasi awal harus berupa tanggal yang valid.',
}),
durasi_akhir: joi.date().min(joi.ref('durasi_awal')).optional().messages({
'date.base': 'Durasi akhir harus berupa tanggal yang valid.',
'date.min': 'Durasi akhir harus setelah atau sama dengan durasi awal.',
}),
paid: joi.string().valid('paid', 'unpaid').optional().messages({
'string.base': 'Status paid harus berupa teks.',
'any.only': 'Status paid hanya boleh "paid" atau "unpaid".',
}),
batch: joi.number().integer().min(1).optional().messages({
'number.base': 'Batch harus berupa angka.',
'number.integer': 'Batch harus berupa bilangan bulat.',
'number.min': 'Batch minimal {#limit}.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
module.exports = {
createLowonganMagangModel,
updateLowonganMagangModel,
};
@@ -0,0 +1,78 @@
const prisma = require('../../../helpers/db/db_connection');
class LowonganMagangRepository {
async create(lowonganData) {
return prisma.lowonganMagang.create({
data: lowonganData,
});
}
async findAll() {
return prisma.lowonganMagang.findMany({
include: {
batch: {
select: {
id: true,
batch_number: true,
is_active: true,
},
},
},
});
}
async findById(id) {
return prisma.lowonganMagang.findUnique({
where: { id: id },
include: {
batch: {
select: {
id: true,
batch_number: true,
is_active: true,
},
},
},
});
}
async findByKelompokPeminatan(kelompok_peminatan) {
return prisma.lowonganMagang.findMany({
where: { kelompok_peminatan },
include: {
batch: {
select: {
id: true,
batch_number: true,
is_active: true,
},
},
},
});
}
async findAllKelompokPeminatan() {
return prisma.lowonganMagang.findMany({
select: {
kelompok_peminatan: true,
},
distinct: ['kelompok_peminatan'],
});
}
async updateById(id, lowonganData) {
return prisma.lowonganMagang.update({
where: { id: id },
data: lowonganData,
});
}
async deleteById(id) {
return prisma.lowonganMagang.delete({
where: { id: id },
});
}
}
module.exports = new LowonganMagangRepository();
@@ -0,0 +1,191 @@
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const lowonganMagangRepository = require('../repositories/lowonganMagang.repository');
const batchRepository = require('../../batch/repositories/batch.repository');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const statusHelper = require('../helpers/status.helper');
const { deleteFileIfExists, validateImagePath } = require('../../../helpers/utils/fileHelper');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError } = require('../../../helpers/error');
class LowonganMagangService {
async createLowonganMagang(lowonganData, imageFile) {
try {
const activeBatch = await batchRepository.findActiveBatch();
if (!activeBatch) {
return error(new NotFoundError('Tidak ada batch yang aktif'));
}
const id = uuidv4();
const imagePath = imageUploadHelper.uploadImage(imageFile);
const status = statusHelper.calculateStatus(
lowonganData.durasi_awal,
lowonganData.durasi_akhir
);
const newLowonganData = {
id,
...lowonganData,
id_batch: activeBatch.id,
status_lowongan: status,
image_path: imagePath,
};
const result = await lowonganMagangRepository.create(newLowonganData);
return data(result);
} catch (err) {
if (imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup file:', cleanupErr);
}
}
return error(err);
}
}
async getAllLowonganMagang() {
try {
const lowonganList = await lowonganMagangRepository.findAll();
if (lowonganList.length === 0) {
return data([]);
}
const updatedList = statusHelper.updateMultipleLowonganStatus(lowonganList);
const validatedList = updatedList.map(lowongan => ({
...lowongan,
image_path: validateImagePath(lowongan.image_path)
}));
return data(validatedList);
} catch (err) {
return error(err);
}
}
async getLowonganMagangById(id) {
try {
const lowongan = await lowonganMagangRepository.findById(id);
if (!lowongan) {
return error(new NotFoundError('Lowongan Magang Tidak Ditemukan'));
}
const updatedLowongan = statusHelper.updateLowonganStatus(lowongan);
updatedLowongan.image_path = validateImagePath(updatedLowongan.image_path);
return data(updatedLowongan);
} catch (err) {
return error(err);
}
}
async getLowonganByKelompokPeminatan(kelompok_peminatan) {
try {
const lowonganList = await lowonganMagangRepository.findByKelompokPeminatan(
kelompok_peminatan
);
if (lowonganList.length === 0) {
return data([]);
}
const updatedList = statusHelper.updateMultipleLowonganStatus(lowonganList);
const validatedList = updatedList.map(lowongan => ({
...lowongan,
image_path: validateImagePath(lowongan.image_path)
}));
return data(validatedList);
} catch (err) {
return error(err);
}
}
async getAllKelompokPeminatan() {
try {
const kelompokList = await lowonganMagangRepository.findAllKelompokPeminatan();
const kelompokNames = kelompokList.map((row) => row.kelompok_peminatan);
return data(kelompokNames);
} catch (err) {
return error(err);
}
}
async updateLowonganMagang(id, lowonganData, imageFile) {
let oldImagePath = null;
let newImageUploaded = false;
try {
const checkResult = await this.getLowonganMagangById(id);
if (checkResult.err) {
return checkResult;
}
const existingLowongan = checkResult.data;
if (imageFile) {
oldImagePath = existingLowongan.image_path;
lowonganData.image_path = imageUploadHelper.uploadImage(imageFile);
newImageUploaded = true;
if (oldImagePath) {
const oldFileName = path.basename(oldImagePath);
await deleteFileIfExists(oldFileName);
}
}
if (lowonganData.durasi_awal || lowonganData.durasi_akhir) {
const durasi_awal = lowonganData.durasi_awal || existingLowongan.durasi_awal;
const durasi_akhir = lowonganData.durasi_akhir || existingLowongan.durasi_akhir;
lowonganData.status_lowongan = statusHelper.calculateStatus(
durasi_awal,
durasi_akhir
);
}
const result = await lowonganMagangRepository.updateById(id, lowonganData);
return data(result);
} catch (err) {
if (newImageUploaded && imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup new file:', cleanupErr);
}
}
return error(err);
}
}
async deleteLowonganMagang(id) {
try {
const checkResult = await this.getLowonganMagangById(id);
if (checkResult.err) {
return checkResult;
}
const lowongan = checkResult.data;
if (lowongan.image_path) {
const fileName = path.basename(lowongan.image_path);
await deleteFileIfExists(fileName);
}
const result = await lowonganMagangRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new LowonganMagangService();
+137
View File
@@ -0,0 +1,137 @@
const mahasiswaService = require('../services/mahasiswa.service');
const { response, error, paginationResponse } = require('../../../helpers/utils/wrapper');
const { InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createMahasiswaModel, updateMahasiswaModel, getAllMahasiswaModel } = require('../models/mahasiswa.model');
class MahasiswaController {
async createMahasiswa(req, res) {
try {
const validatePayload = isValidPayload(req.body, createMahasiswaModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await mahasiswaService.createMahasiswa(validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Mahasiswa berhasil dibuat', SUCCESS.CREATED);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getAllMahasiswa(req, res) {
try {
const validatePayload = isValidPayload(req.query, getAllMahasiswaModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const query = validatePayload.data;
const result = await mahasiswaService.getAllMahasiswa({ query });
if (result.err) {
return response(res, 'fail', result);
}
return paginationResponse(res, 'success', result, 'Data mahasiswa berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getMahasiswaById(req, res) {
try {
const { id } = req.params;
const result = await mahasiswaService.getMahasiswaById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Mahasiswa berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updateMahasiswa(req, res) {
try {
const { id } = req.params;
const validatePayload = isValidPayload(req.body, updateMahasiswaModel);
if (validatePayload.err) {
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const result = await mahasiswaService.updateMahasiswa(id, validatePayload.data);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Mahasiswa berhasil diperbarui', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteMahasiswa(req, res) {
try {
const { id } = req.params;
const result = await mahasiswaService.deleteMahasiswa(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Mahasiswa berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deleteAllMahasiswa(req, res) {
try {
const result = await mahasiswaService.deleteAllMahasiswa();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Semua mahasiswa berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new MahasiswaController();
+9
View File
@@ -0,0 +1,9 @@
const mahasiswaController = require('./controllers/mahasiswa.controller');
const mahasiswaService = require('./services/mahasiswa.service');
const mahasiswaRepository = require('./repositories/mahasiswa.repository');
module.exports = {
mahasiswaController,
mahasiswaService,
mahasiswaRepository
};
+159
View File
@@ -0,0 +1,159 @@
const joi = require('joi');
const phoneRegex = /^(\+62|62|0)8[1-9][0-9]{6,11}$/;
const createMahasiswaModel = joi.object().keys({
nama_depan: joi.string().max(255).required().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.empty': 'Nama depan tidak boleh kosong.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
'any.required': 'Nama depan wajib diisi.',
}),
nama_belakang: joi.string().max(255).optional().allow('', null).messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).required().messages({
'string.base': 'Email harus berupa teks.',
'string.empty': 'Email tidak boleh kosong.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
'any.required': 'Email wajib diisi.',
}),
kontak: joi.string().pattern(phoneRegex).max(255).required().messages({
'string.base': 'Kontak harus berupa teks.',
'string.empty': 'Kontak tidak boleh kosong.',
'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).',
'string.max': 'Kontak maksimal {#limit} karakter.',
'any.required': 'Kontak wajib diisi.',
}),
jurusan: joi.string().max(255).required().messages({
'string.base': 'Jurusan harus berupa teks.',
'string.empty': 'Jurusan tidak boleh kosong.',
'string.max': 'Jurusan maksimal {#limit} karakter.',
'any.required': 'Jurusan wajib diisi.',
}),
universitas: joi.string().max(255).required().messages({
'string.base': 'Universitas harus berupa teks.',
'string.empty': 'Universitas tidak boleh kosong.',
'string.max': 'Universitas maksimal {#limit} karakter.',
'any.required': 'Universitas wajib diisi.',
}),
negara: joi.string().max(255).required().messages({
'string.base': 'Negara harus berupa teks.',
'string.empty': 'Negara tidak boleh kosong.',
'string.max': 'Negara maksimal {#limit} karakter.',
'any.required': 'Negara wajib diisi.',
}),
role: joi.string().valid('student').default('student').messages({
'string.base': 'Role harus berupa teks.',
'any.only': 'Role hanya boleh "student".',
}),
cv_path: joi.string().required().messages({
'string.base': 'Path CV harus berupa teks.',
'string.empty': 'Path CV tidak boleh kosong.',
'any.required': 'CV wajib diupload.',
}),
portofolio_path: joi.string().required().messages({
'string.base': 'Path portofolio harus berupa teks.',
'string.empty': 'Path portofolio tidak boleh kosong.',
'any.required': 'Portofolio wajib diupload.',
}),
motivasi: joi.string().required().messages({
'string.base': 'Motivasi harus berupa teks.',
'string.empty': 'Motivasi tidak boleh kosong.',
'any.required': 'Motivasi wajib diisi.',
}),
relevant_skills: joi.string().max(255).required().messages({
'string.base': 'Keahlian yang relevan harus berupa teks.',
'string.empty': 'Keahlian yang relevan tidak boleh kosong.',
'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.',
'any.required': 'Keahlian yang relevan wajib diisi.',
}),
});
const updateMahasiswaModel = joi.object().keys({
nama_depan: joi.string().max(255).optional().messages({
'string.base': 'Nama depan harus berupa teks.',
'string.max': 'Nama depan maksimal {#limit} karakter.',
}),
nama_belakang: joi.string().max(255).optional().allow('', null).messages({
'string.base': 'Nama belakang harus berupa teks.',
'string.max': 'Nama belakang maksimal {#limit} karakter.',
}),
email: joi.string().email({ tlds: { allow: false } }).max(255).optional().messages({
'string.base': 'Email harus berupa teks.',
'string.email': 'Format email tidak valid.',
'string.max': 'Email maksimal {#limit} karakter.',
}),
kontak: joi.string().pattern(phoneRegex).max(255).optional().messages({
'string.base': 'Kontak harus berupa teks.',
'string.pattern.base': 'Format nomor kontak tidak valid. Gunakan format Indonesia (08xx atau +628xx).',
'string.max': 'Kontak maksimal {#limit} karakter.',
}),
jurusan: joi.string().max(255).optional().messages({
'string.base': 'Jurusan harus berupa teks.',
'string.max': 'Jurusan maksimal {#limit} karakter.',
}),
universitas: joi.string().max(255).optional().messages({
'string.base': 'Universitas harus berupa teks.',
'string.max': 'Universitas maksimal {#limit} karakter.',
}),
negara: joi.string().max(255).optional().messages({
'string.base': 'Negara harus berupa teks.',
'string.max': 'Negara maksimal {#limit} karakter.',
}),
cv_path: joi.string().optional().messages({
'string.base': 'Path CV harus berupa teks.',
}),
portofolio_path: joi.string().optional().messages({
'string.base': 'Path portofolio harus berupa teks.',
}),
motivasi: joi.string().optional().messages({
'string.base': 'Motivasi harus berupa teks.',
}),
relevant_skills: joi.string().max(255).optional().messages({
'string.base': 'Keahlian yang relevan harus berupa teks.',
'string.max': 'Keahlian yang relevan maksimal {#limit} karakter.',
}),
});
const getAllMahasiswaModel = joi.object().keys({
page: joi.number().integer().min(1).default(1).messages({
'number.base': 'Halaman harus berupa angka.',
'number.integer': 'Halaman harus berupa angka bulat.',
'number.min': 'Halaman minimal adalah {#limit}.',
}),
limit: joi.number().integer().min(1).default(10).messages({
'number.base': 'Limit harus berupa angka.',
'number.integer': 'Limit harus berupa angka bulat.',
'number.min': 'Limit minimal adalah {#limit}.',
}),
});
module.exports = {
createMahasiswaModel,
updateMahasiswaModel,
getAllMahasiswaModel
};
+65
View File
@@ -0,0 +1,65 @@
const prisma = require('../../../helpers/db/db_connection');
class MahasiswaRepository {
async create(mahasiswaData) {
return prisma.mahasiswa.create({
data: mahasiswaData,
});
}
async findAll({ offset, limit }) {
return prisma.mahasiswa.findMany({
skip: offset,
take: limit,
});
}
async findById(id) {
return prisma.mahasiswa.findUnique({
where: { id: parseInt(id) },
include: {
lamaran_magang: {
include: {
lowongan_magang: {
select: {
posisi: true,
kelompok_peminatan: true,
durasi_awal: true,
durasi_akhir: true,
},
},
},
},
},
});
}
async findByEmail(email) {
return prisma.mahasiswa.findUnique({
where: { email },
});
}
async updateById(id, mahasiswaData) {
return prisma.mahasiswa.update({
where: { id: parseInt(id) },
data: mahasiswaData,
});
}
async deleteById(id) {
return prisma.mahasiswa.delete({
where: { id: parseInt(id) },
});
}
async deleteAll() {
return prisma.mahasiswa.deleteMany();
}
async countAllMahasiswa() {
return prisma.mahasiswa.count();
}
}
module.exports = new MahasiswaRepository();
+119
View File
@@ -0,0 +1,119 @@
const mahasiswaRepository = require('../repositories/mahasiswa.repository');
const { data, error } = require('../../../helpers/utils/wrapper');
const { BadRequestError, NotFoundError } = require('../../../helpers/error');
const { paginationData } = require("../../../helpers/utils/wrapper");
class MahasiswaService {
async createMahasiswa(mahasiswaData) {
try {
const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaData.email);
if (existingMahasiswa) {
return error(new BadRequestError('Email Sudah Terdaftar'));
}
if (!mahasiswaData.role) {
mahasiswaData.role = 'student';
}
const result = await mahasiswaRepository.create(mahasiswaData);
return data(result);
} catch (err) {
return error(err);
}
}
async getAllMahasiswa({ page = 1, limit = 10 }) {
try {
const offset = (page - 1) * limit;
const mahasiswa = await mahasiswaRepository.findAll({ offset, limit });
if (!mahasiswa || mahasiswa.length === 0) {
return error(new NotFoundError('Tidak ada mahasiswa ditemukan'));
}
const total = await mahasiswaRepository.countAllMahasiswa();
const totalPages = Math.ceil(total / limit);
const meta = { page: Number(page), limit: Number(limit), total, totalPages };
return paginationData(mahasiswa, meta);
} catch (err) {
return error(err);
}
}
async getMahasiswaById(id) {
try {
const mahasiswa = await mahasiswaRepository.findById(id);
if (!mahasiswa) {
return error(new NotFoundError('Mahasiswa Tidak Ditemukan'));
}
return data(mahasiswa);
} catch (err) {
return error(err);
}
}
async getMahasiswaByEmail(email) {
try {
const mahasiswa = await mahasiswaRepository.findByEmail(email);
if (!mahasiswa) {
return error(new NotFoundError('Mahasiswa Tidak Ditemukan'));
}
return data(mahasiswa);
} catch (err) {
return error(err);
}
}
async updateMahasiswa(id, mahasiswaData) {
try {
const checkResult = await this.getMahasiswaById(id);
if (checkResult.err) {
return checkResult;
}
if (mahasiswaData.email) {
const existingMahasiswa = await mahasiswaRepository.findByEmail(mahasiswaData.email);
if (existingMahasiswa && existingMahasiswa.id !== id) {
return error(new BadRequestError('Email Sudah Terdaftar'));
}
}
const result = await mahasiswaRepository.updateById(id, mahasiswaData);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteMahasiswa(id) {
try {
const checkResult = await this.getMahasiswaById(id);
if (checkResult.err) {
return checkResult;
}
const result = await mahasiswaRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
async deleteAllMahasiswa() {
try {
const result = await mahasiswaRepository.deleteAll();
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new MahasiswaService();
@@ -0,0 +1,128 @@
const partnershipService = require('../services/partnership.service');
const { response, error } = require('../../../helpers/utils/wrapper');
const { InternalServerError } = require('../../../helpers/error');
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
const { isValidPayload } = require('../../../helpers/utils/validator');
const { createPartnershipModel, updatePartnershipModel } = require('../models/partnership.model');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
class PartnershipController {
async addPartnership(req, res) {
try {
const image = req.file;
const validatePayload = isValidPayload(req.body, createPartnershipModel);
if (validatePayload.err) {
if (image?.filename) {
await deleteFileIfExists(image.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { nama_partner } = validatePayload.data;
const result = await partnershipService.createPartnership(nama_partner, image);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data partnership berhasil ditambahkan', SUCCESS.CREATED);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getPartnership(req, res) {
try {
const result = await partnershipService.getAllPartnerships();
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data partnership berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async getPartnershipById(req, res) {
try {
const { id } = req.params;
const result = await partnershipService.getPartnershipById(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data partnership berhasil diambil', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async updatePartnership(req, res) {
try {
const { id } = req.params;
const image = req.file;
const validatePayload = isValidPayload(req.body, updatePartnershipModel);
if (validatePayload.err) {
if (image?.filename) {
await deleteFileIfExists(image.filename);
}
return response(
res,
'fail',
{ err: validatePayload.err, data: null },
'Data tidak valid',
ERROR.EXPECTATION_FAILED
);
}
const { nama_partner } = validatePayload.data;
const result = await partnershipService.updatePartnership(id, nama_partner, image);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data partnership berhasil diperbarui', SUCCESS.OK);
} catch (err) {
if (req.file?.filename) {
await deleteFileIfExists(req.file.filename);
}
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
async deletePartnership(req, res) {
try {
const { id } = req.params;
const result = await partnershipService.deletePartnership(id);
if (result.err) {
return response(res, 'fail', result);
}
return response(res, 'success', result, 'Data partnership berhasil dihapus', SUCCESS.OK);
} catch (err) {
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
}
}
}
module.exports = new PartnershipController();
+9
View File
@@ -0,0 +1,9 @@
const partnershipController = require('./controllers/partnership.controller');
const partnershipService = require('./services/partnership.service');
const partnershipRepository = require('./repositories/partnership.repository');
module.exports = {
partnershipController,
partnershipService,
partnershipRepository
};
+30
View File
@@ -0,0 +1,30 @@
const joi = require('joi');
const createPartnershipModel = joi.object().keys({
nama_partner: joi.string().max(255).required().messages({
'string.base': 'Nama partner harus berupa teks.',
'string.empty': 'Nama partner tidak boleh kosong.',
'string.max': 'Nama partner maksimal {#limit} karakter.',
'any.required': 'Nama partner wajib diisi.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
const updatePartnershipModel = joi.object().keys({
nama_partner: joi.string().max(255).optional().messages({
'string.base': 'Nama partner harus berupa teks.',
'string.max': 'Nama partner maksimal {#limit} karakter.',
}),
image_path: joi.string().optional().allow('', null).messages({
'string.base': 'Path gambar harus berupa teks.',
}),
});
module.exports = {
createPartnershipModel,
updatePartnershipModel,
};
@@ -0,0 +1,34 @@
const prisma = require('../../../helpers/db/db_connection');
class PartnershipRepository {
async create(partnershipData) {
return prisma.partnership.create({
data: partnershipData,
});
}
async findAll() {
return prisma.partnership.findMany();
}
async findById(id) {
return prisma.partnership.findUnique({
where: { id: parseInt(id) },
});
}
async updateById(id, partnershipData) {
return prisma.partnership.update({
where: { id: parseInt(id) },
data: partnershipData,
});
}
async deleteById(id) {
return prisma.partnership.delete({
where: { id: parseInt(id) },
});
}
}
module.exports = new PartnershipRepository();
+121
View File
@@ -0,0 +1,121 @@
const partnershipRepository = require('../repositories/partnership.repository');
const path = require('path');
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
const { data, error } = require('../../../helpers/utils/wrapper');
const { NotFoundError } = require('../../../helpers/error');
class PartnershipService {
async createPartnership(nama_partner, imageFile) {
try {
const imagePath = imageUploadHelper.uploadImage(imageFile);
const partnershipData = {
nama_partner,
image_path: imagePath,
};
const result = await partnershipRepository.create(partnershipData);
return data(result);
} catch (err) {
if (imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup file:', cleanupErr);
}
}
return error(err);
}
}
async getAllPartnerships() {
try {
const partnerships = await partnershipRepository.findAll();
return data(partnerships);
} catch (err) {
return error(err);
}
}
async getPartnershipById(id) {
try {
const partnership = await partnershipRepository.findById(id);
if (!partnership) {
return error(new NotFoundError('Partnership Tidak Ditemukan'));
}
return data(partnership);
} catch (err) {
return error(err);
}
}
async updatePartnership(id, nama_partner, imageFile) {
let oldImagePath = null;
let newImageUploaded = false;
try {
const checkResult = await this.getPartnershipById(id);
if (checkResult.err) {
return checkResult;
}
const existingPartnership = checkResult.data;
let imagePath = existingPartnership.image_path;
if (imageFile) {
oldImagePath = existingPartnership.image_path;
imagePath = imageUploadHelper.uploadImage(imageFile);
newImageUploaded = true;
if (oldImagePath) {
const oldFileName = path.basename(oldImagePath);
await deleteFileIfExists(oldFileName);
}
}
const partnershipData = {
nama_partner,
image_path: imagePath,
};
const result = await partnershipRepository.updateById(id, partnershipData);
return data(result);
} catch (err) {
if (newImageUploaded && imageFile?.filename) {
try {
await deleteFileIfExists(imageFile.filename);
} catch (cleanupErr) {
console.error('Failed to cleanup new file:', cleanupErr);
}
}
return error(err);
}
}
async deletePartnership(id) {
try {
const checkResult = await this.getPartnershipById(id);
if (checkResult.err) {
return checkResult;
}
const partnership = checkResult.data;
if (partnership.image_path) {
const fileName = path.basename(partnership.image_path);
await deleteFileIfExists(fileName);
}
const result = await partnershipRepository.deleteById(id);
return data(result);
} catch (err) {
return error(err);
}
}
}
module.exports = new PartnershipService();