refactor: rewrite ke codebase lama
This commit is contained in:
@@ -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
@@ -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
@@ -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();
|
||||
Executable
+11
@@ -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
@@ -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();
|
||||
Reference in New Issue
Block a user