From c31c91fb8a54216030fa65da8f1f6762b020b1a3 Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Wed, 1 Jul 2026 18:05:17 +0700 Subject: [PATCH 1/3] feat(lamaran): reset lamaran dan rollover batch, serta menambahkan snapshot posisi lamaran mhs ke tabel user --- .../migration.sql | 17 +++ prisma/schema.prisma | 28 ++-- .../controllers/lamaranMagang.controller.js | 26 ++++ .../repositories/lamaranMagang.repository.js | 132 +++++++++++++++++- .../services/lamaranMagang.service.js | 53 +++++++ src/routes/lamaranMagang.routes.js | 84 ++++++++--- 6 files changed, 309 insertions(+), 31 deletions(-) create mode 100644 prisma/migrations/20260701104142_preserve_intern_after_application_cleanup/migration.sql diff --git a/prisma/migrations/20260701104142_preserve_intern_after_application_cleanup/migration.sql b/prisma/migrations/20260701104142_preserve_intern_after_application_cleanup/migration.sql new file mode 100644 index 0000000..d27002e --- /dev/null +++ b/prisma/migrations/20260701104142_preserve_intern_after_application_cleanup/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE `user` DROP FOREIGN KEY `user_id_lamaran_magang_foreign`; + +-- DropForeignKey +ALTER TABLE `user` DROP FOREIGN KEY `user_id_mahasiswa_foreign`; + +-- AlterTable +ALTER TABLE `user` ADD COLUMN `intern_interest_group` VARCHAR(255) NULL, + ADD COLUMN `intern_position` VARCHAR(255) NULL, + MODIFY `id_mahasiswa` INTEGER NULL, + MODIFY `id_lamaran_magang` INTEGER NULL; + +-- AddForeignKey +ALTER TABLE `user` ADD CONSTRAINT `user_id_mahasiswa_foreign` FOREIGN KEY (`id_mahasiswa`) REFERENCES `mahasiswa`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user` ADD CONSTRAINT `user_id_lamaran_magang_foreign` FOREIGN KEY (`id_lamaran_magang`) REFERENCES `lamaran_magang`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d04e0b2..13b8a48 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -179,20 +179,22 @@ model Batch { } model User { - id Int @id @default(autoincrement()) - id_mahasiswa Int @unique - id_lamaran_magang Int @unique - full_name String @db.VarChar(255) - email String @unique @db.VarChar(255) - password String @db.Text - role Role @default(intern) - professional_bio String? @db.Text - is_active Boolean @default(true) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + id Int @id @default(autoincrement()) + id_mahasiswa Int? @unique + id_lamaran_magang Int? @unique + full_name String @db.VarChar(255) + email String @unique @db.VarChar(255) + password String @db.Text + role Role @default(intern) + professional_bio String? @db.Text + intern_position String? @db.VarChar(255) + intern_interest_group String? @db.VarChar(255) + is_active Boolean @default(true) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt - mahasiswa Mahasiswa @relation(fields: [id_mahasiswa], references: [id], map: "user_id_mahasiswa_foreign") - lamaran_magang LamaranMagang @relation(fields: [id_lamaran_magang], references: [id], map: "user_id_lamaran_magang_foreign") + mahasiswa Mahasiswa? @relation(fields: [id_mahasiswa], references: [id], onDelete: SetNull, map: "user_id_mahasiswa_foreign") + lamaran_magang LamaranMagang? @relation(fields: [id_lamaran_magang], references: [id], onDelete: SetNull, map: "user_id_lamaran_magang_foreign") project_members ProjectMember[] task_submissions TaskSubmission[] certificates Certificate[] diff --git a/src/modules/lamaranMagang/controllers/lamaranMagang.controller.js b/src/modules/lamaranMagang/controllers/lamaranMagang.controller.js index 9ab9265..e9f96b6 100755 --- a/src/modules/lamaranMagang/controllers/lamaranMagang.controller.js +++ b/src/modules/lamaranMagang/controllers/lamaranMagang.controller.js @@ -172,6 +172,32 @@ class LamaranMagangController { } } + async deleteAllLamaranMagang(req, res) { + try { + const result = await lamaranMagangService.deleteAllLamaranMagang(); + + if (result.err) { + return response(res, 'fail', result); + } + + return response( + res, + 'success', + result, + 'Data lamaran magang dan mahasiswa berhasil dibersihkan, batch baru berhasil dibuat', + 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(); diff --git a/src/modules/lamaranMagang/repositories/lamaranMagang.repository.js b/src/modules/lamaranMagang/repositories/lamaranMagang.repository.js index 69af92b..1fc6be8 100755 --- a/src/modules/lamaranMagang/repositories/lamaranMagang.repository.js +++ b/src/modules/lamaranMagang/repositories/lamaranMagang.repository.js @@ -163,6 +163,134 @@ class LamaranMagangRepository { }); } + async findAllMahasiswaForCleanup() { + return prisma.mahasiswa.findMany({ + select: { + id: true, + cv_path: true, + portofolio_path: true, + }, + }); + } + + async deleteAllLamaranMagang() { + return prisma.$transaction(async (tx) => { + const internUsers = await tx.user.findMany({ + where: { + role: "intern", + id_lamaran_magang: { + not: null, + }, + }, + select: { + id: true, + intern_position: true, + intern_interest_group: true, + lamaran_magang: { + select: { + lowongan_magang: { + select: { + posisi: true, + kelompok_peminatan: true, + }, + }, + }, + }, + }, + }); + + for (const user of internUsers) { + const lowongan = user.lamaran_magang?.lowongan_magang; + + await tx.user.update({ + where: { + id: user.id, + }, + data: { + intern_position: user.intern_position || lowongan?.posisi || null, + intern_interest_group: + user.intern_interest_group || lowongan?.kelompok_peminatan || null, + }, + }); + } + + const detachedUsers = await tx.user.updateMany({ + where: { + OR: [ + { + id_mahasiswa: { + not: null, + }, + }, + { + id_lamaran_magang: { + not: null, + }, + }, + ], + }, + data: { + id_mahasiswa: null, + id_lamaran_magang: null, + }, + }); + + const deletedLamaranMagang = await tx.lamaranMagang.deleteMany({}); + const deletedMahasiswa = await tx.mahasiswa.deleteMany({}); + + const latestBatch = await tx.batch.findFirst({ + orderBy: { + batch_number: "desc", + }, + }); + + const previousActiveBatch = await tx.batch.findFirst({ + where: { + is_active: true, + }, + orderBy: { + batch_number: "desc", + }, + }); + + if (previousActiveBatch) { + await tx.batch.update({ + where: { + id: previousActiveBatch.id, + }, + data: { + is_active: false, + }, + }); + } + + const newBatch = await tx.batch.create({ + data: { + batch_number: latestBatch ? latestBatch.batch_number + 1 : 1, + is_active: true, + }, + }); + + return { + detached_users: detachedUsers.count, + deleted_lamaran_magang: deletedLamaranMagang.count, + deleted_mahasiswa: deletedMahasiswa.count, + previous_active_batch: previousActiveBatch + ? { + id: previousActiveBatch.id, + batch_number: previousActiveBatch.batch_number, + is_active: false, + } + : null, + new_active_batch: { + id: newBatch.id, + batch_number: newBatch.batch_number, + is_active: newBatch.is_active, + }, + }; + }); + } + async getStatistics({ query }) { let where = {}; @@ -238,7 +366,7 @@ class LamaranMagangRepository { : Prisma.sql``; const data = await prisma.$queryRaw` - SELECT + SELECT m.negara, COUNT(*) as total, SUM(lm.status = 'diterima') as diterima, @@ -266,7 +394,7 @@ class LamaranMagangRepository { : Prisma.sql``; const data = await prisma.$queryRaw` - SELECT + SELECT m.universitas, COUNT(*) as total, SUM(lm.status = 'diterima') as diterima, diff --git a/src/modules/lamaranMagang/services/lamaranMagang.service.js b/src/modules/lamaranMagang/services/lamaranMagang.service.js index 873e02e..f769385 100755 --- a/src/modules/lamaranMagang/services/lamaranMagang.service.js +++ b/src/modules/lamaranMagang/services/lamaranMagang.service.js @@ -202,6 +202,10 @@ class LamaranMagangService { let generatedPassword = null; if (status === "diterima") { + // Menyimpan posisi dan kelompok peminatan intern + const internPosition = checkResult.data.lowongan_magang?.posisi || null; + const internInterestGroup = checkResult.data.lowongan_magang?.kelompok_peminatan || null; + const existingUser = await prisma.user.findFirst({ where: { OR: [ @@ -227,9 +231,21 @@ class LamaranMagangService { email: checkResult.data.mahasiswa.email, password: hashedPassword, role: "intern", + intern_position: internPosition, + intern_interest_group: internInterestGroup, is_active: true, }, }); + } else { + await prisma.user.update({ + where: { + id: existingUser.id, + }, + data: { + intern_position: existingUser.intern_position || internPosition, + intern_interest_group:existingUser.intern_interest_group || internInterestGroup, + }, + }); } } @@ -267,6 +283,43 @@ class LamaranMagangService { } } + async deleteAllLamaranMagang() { + try { + const mahasiswaList = await lamaranMagangRepository.findAllMahasiswaForCleanup(); + const result = await lamaranMagangRepository.deleteAllLamaranMagang(); + + let deletedFiles = 0; + const failedFiles = []; + + for (const mahasiswa of mahasiswaList) { + const files = [mahasiswa.cv_path, mahasiswa.portofolio_path]; + + for (const filePath of files) { + if (!filePath) continue; + + try { + const fileName = path.basename(filePath); + await deleteFileIfExists(fileName); + deletedFiles += 1; + } catch (fileErr) { + failedFiles.push({ + file_path: filePath, + error: fileErr.message, + }); + } + } + } + + return data({ + ...result, + deleted_files: deletedFiles, + failed_files: failedFiles, + }); + } catch (err) { + return error(err); + } + } + async exportToExcel() { try { const lamaranList = await lamaranMagangRepository.findAll({}); diff --git a/src/routes/lamaranMagang.routes.js b/src/routes/lamaranMagang.routes.js index 8fa981b..c2adc02 100755 --- a/src/routes/lamaranMagang.routes.js +++ b/src/routes/lamaranMagang.routes.js @@ -441,35 +441,87 @@ router.delete('/delete/:id', verifyJWT, isAdmin, lamaranMagangController.deleteL * @swagger * /lamaran-magang-api/delete: * delete: - * summary: Delete all internship applications, students, and their files + * summary: Reset internship applications and create new active batch + * description: Delete all internship applications and mahasiswa data, detach existing intern accounts from deleted application records, preserve intern accounts and their position snapshots, deactivate the previous active batch, and create one new active batch. Only admin can access this endpoint. * tags: [Lamaran Magang] * security: * - bearerAuth: [] * responses: * 200: - * description: All student data, applications, and files were successfully deleted. + * description: Application data reset successfully and new batch created. * content: * application/json: * schema: * type: object * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * properties: + * detached_users: + * type: integer + * example: 10 + * deleted_lamaran_magang: + * type: integer + * example: 25 + * deleted_mahasiswa: + * type: integer + * example: 25 + * deleted_files: + * type: integer + * example: 50 + * failed_files: + * type: array + * items: + * type: object + * properties: + * file_path: + * type: string + * example: /uploads/cv/example.pdf + * error: + * type: string + * example: File not found + * previous_active_batch: + * type: object + * nullable: true + * properties: + * id: + * type: integer + * example: 3 + * batch_number: + * type: integer + * example: 3 + * is_active: + * type: boolean + * example: false + * new_active_batch: + * type: object + * properties: + * id: + * type: integer + * example: 4 + * batch_number: + * type: integer + * example: 4 + * is_active: + * type: boolean + * example: true * message: * type: string - * example: All student data, applications, and files were successfully deleted. + * example: Data lamaran magang dan mahasiswa berhasil dibersihkan, batch baru berhasil dibuat + * code: + * type: integer + * example: 200 + * 401: + * description: Unauthorized + * 403: + * description: Forbidden. Only admin can access this endpoint. * 500: - * description: Failed to delete all data - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Failed to delete all data. - * error: - * type: string + * description: Failed to reset internship application data. */ -// router.delete("/delete", verifyJWT, lamaranMagangController.deleteAllLamaranMagang); // Method doesn't exist +router.delete("/delete", verifyJWT, isAdmin, lamaranMagangController.deleteAllLamaranMagang); /** * @swagger @@ -683,4 +735,4 @@ router.get('/statistics/university', verifyJWT, isAdmin, lamaranMagangController -module.exports = router; \ No newline at end of file +module.exports = router; From b21f5ad5d9b545ec4e4fc31f64ecb20b9c16ab5b Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Wed, 1 Jul 2026 18:30:04 +0700 Subject: [PATCH 2/3] fix(intern): menerapkan fallback posisi intern ke snapshot tabal user --- src/modules/auth/services/auth.service.js | 15 +++++++++++++-- .../project/repositories/project.repository.js | 2 ++ src/modules/project/services/project.service.js | 6 +++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/modules/auth/services/auth.service.js b/src/modules/auth/services/auth.service.js index c6b0bcb..8040a69 100755 --- a/src/modules/auth/services/auth.service.js +++ b/src/modules/auth/services/auth.service.js @@ -124,8 +124,13 @@ class AuthService { } const { password, ...userWithoutPassword } = user; + const lowongan = user.lamaran_magang?.lowongan_magang; - return data(userWithoutPassword); + return data({ + ...userWithoutPassword, + position: lowongan?.posisi || user.intern_position || null, + kelompok_peminatan: lowongan?.kelompok_peminatan || user.intern_interest_group || null, + }); } else { return error(new NotFoundError('Role tidak valid')); } @@ -262,7 +267,13 @@ class AuthService { }); const { password: _, ...userWithoutPassword } = updatedUser; - return data(userWithoutPassword); + const lowongan = updatedUser.lamaran_magang?.lowongan_magang; + + return data({ + ...userWithoutPassword, + position: lowongan?.posisi || updatedUser.intern_position || null, + kelompok_peminatan: lowongan?.kelompok_peminatan || updatedUser.intern_interest_group || null, + }); } else { if (imageFile?.filename) { const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper'); diff --git a/src/modules/project/repositories/project.repository.js b/src/modules/project/repositories/project.repository.js index e5470ac..e18b641 100644 --- a/src/modules/project/repositories/project.repository.js +++ b/src/modules/project/repositories/project.repository.js @@ -184,6 +184,8 @@ class ProjectRepository { full_name: true, email: true, professional_bio: true, + intern_position: true, + intern_interest_group: true, is_active: true, lamaran_magang: { select: { diff --git a/src/modules/project/services/project.service.js b/src/modules/project/services/project.service.js index 1d26c9d..4aa6f53 100644 --- a/src/modules/project/services/project.service.js +++ b/src/modules/project/services/project.service.js @@ -393,8 +393,8 @@ class ProjectService { full_name: user.full_name, email: user.email, professional_bio: user.professional_bio || null, - position: lowongan?.posisi || null, - kelompok_peminatan: lowongan?.kelompok_peminatan || null, + position: lowongan?.posisi || user.intern_position || null, + kelompok_peminatan: lowongan?.kelompok_peminatan || user.intern_interest_group || null, }, created_at: member.created_at, updated_at: member.updated_at, @@ -540,7 +540,7 @@ class ProjectService { const mappedInterns = interns.map((intern) => { const activeMembership = intern.project_members?.[0] || null; const projectName = activeMembership?.project?.project_name || "Unassigned"; - const role = intern.lamaran_magang?.lowongan_magang?.posisi || "-"; + const role = intern.lamaran_magang?.lowongan_magang?.posisi || intern.intern_position || "-"; return { id: intern.id, From 811025e9e4ff2c4119e0922d04d255d7b27598b4 Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Wed, 1 Jul 2026 20:19:36 +0700 Subject: [PATCH 3/3] fix(lamaran): reuse akun intern yang sudah ada dan update data ke lamaran magang terbaru --- .../lamaranMagang/services/lamaranMagang.service.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/modules/lamaranMagang/services/lamaranMagang.service.js b/src/modules/lamaranMagang/services/lamaranMagang.service.js index f769385..383b415 100755 --- a/src/modules/lamaranMagang/services/lamaranMagang.service.js +++ b/src/modules/lamaranMagang/services/lamaranMagang.service.js @@ -237,13 +237,21 @@ class LamaranMagangService { }, }); } else { + const { nama_depan, nama_belakang } = checkResult.data.mahasiswa; + const fullName = [nama_depan, nama_belakang].filter(Boolean).join(" "); + await prisma.user.update({ where: { id: existingUser.id, }, data: { - intern_position: existingUser.intern_position || internPosition, - intern_interest_group:existingUser.intern_interest_group || internInterestGroup, + id_mahasiswa: checkResult.data.id_mahasiswa, + id_lamaran_magang: parseInt(id_lamaran_magang), + full_name: fullName, + email: checkResult.data.mahasiswa.email, + intern_position: internPosition, + intern_interest_group: internInterestGroup, + is_active: true }, }); }