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
@@ -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();