Merge pull request #6 from internship-humic/feat/admin-project-lms
feat/admin project lms
This commit is contained in:
Generated
+1
@@ -2961,6 +2961,7 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.16.2",
|
||||
"@prisma/engines": "6.16.2"
|
||||
|
||||
@@ -37,5 +37,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^6.16.2"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "node prisma/seed.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `admin` ADD COLUMN `professional_bio` TEXT NULL,
|
||||
ADD COLUMN `profile_picture` TEXT NULL,
|
||||
MODIFY `role` ENUM('admin', 'intern') NOT NULL DEFAULT 'admin';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `user` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`id_mahasiswa` INTEGER NOT NULL,
|
||||
`id_lamaran_magang` INTEGER NOT NULL,
|
||||
`full_name` VARCHAR(255) NOT NULL,
|
||||
`email` VARCHAR(255) NOT NULL,
|
||||
`password` TEXT NOT NULL,
|
||||
`role` ENUM('admin', 'intern') NOT NULL DEFAULT 'intern',
|
||||
`professional_bio` TEXT NULL,
|
||||
`is_active` BOOLEAN NOT NULL DEFAULT true,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `user_id_mahasiswa_key`(`id_mahasiswa`),
|
||||
UNIQUE INDEX `user_id_lamaran_magang_key`(`id_lamaran_magang`),
|
||||
UNIQUE INDEX `user_email_key`(`email`),
|
||||
INDEX `user_email_idx`(`email`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `project` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`id_admin` INTEGER NOT NULL,
|
||||
`project_icon` ENUM('code', 'chart', 'cloud', 'mobile', 'gear', 'users', 'clipboard', 'speedometer', 'lightbulb', 'shield') NOT NULL DEFAULT 'code',
|
||||
`project_name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`start_date` DATE NOT NULL,
|
||||
`end_date` DATE NOT NULL,
|
||||
`max_members` INTEGER NOT NULL DEFAULT 8,
|
||||
`status` ENUM('active', 'completed', 'archived') NOT NULL DEFAULT 'active',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `project_id_admin_foreign`(`id_admin`),
|
||||
INDEX `project_status_idx`(`status`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `project_member` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`id_project` INTEGER NOT NULL,
|
||||
`id_user` INTEGER NOT NULL,
|
||||
`status` ENUM('active', 'removed') NOT NULL DEFAULT 'active',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `project_member_id_project_foreign`(`id_project`),
|
||||
INDEX `project_member_id_user_foreign`(`id_user`),
|
||||
INDEX `project_member_status_idx`(`status`),
|
||||
UNIQUE INDEX `project_member_id_project_id_user_key`(`id_project`, `id_user`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `task` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`id_project` INTEGER NOT NULL,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`deadline_at` DATETIME(3) NOT NULL,
|
||||
`submission_type` ENUM('file_upload', 'url_link') NOT NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `task_id_project_foreign`(`id_project`),
|
||||
INDEX `task_deadline_at_idx`(`deadline_at`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `task_submission` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`id_task` INTEGER NOT NULL,
|
||||
`id_user` INTEGER NOT NULL,
|
||||
`file_path` TEXT NULL,
|
||||
`url_link` TEXT NULL,
|
||||
`submitted_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `task_submission_id_task_foreign`(`id_task`),
|
||||
INDEX `task_submission_id_user_foreign`(`id_user`),
|
||||
UNIQUE INDEX `task_submission_id_task_id_user_key`(`id_task`, `id_user`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `user` ADD CONSTRAINT `user_id_mahasiswa_foreign` FOREIGN KEY (`id_mahasiswa`) REFERENCES `mahasiswa`(`id`) ON DELETE RESTRICT 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 RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `project` ADD CONSTRAINT `project_id_admin_foreign` FOREIGN KEY (`id_admin`) REFERENCES `admin`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `project_member` ADD CONSTRAINT `project_member_id_project_foreign` FOREIGN KEY (`id_project`) REFERENCES `project`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `project_member` ADD CONSTRAINT `project_member_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `task` ADD CONSTRAINT `task_id_project_foreign` FOREIGN KEY (`id_project`) REFERENCES `project`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `task_submission` ADD CONSTRAINT `task_submission_id_task_foreign` FOREIGN KEY (`id_task`) REFERENCES `task`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `task_submission` ADD CONSTRAINT `task_submission_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -23,6 +23,11 @@ model Admin {
|
||||
password String @db.Text
|
||||
signature String? @db.Text // singature untuk identifikasi auth jwt (best practice)
|
||||
|
||||
profile_picture String? @db.Text
|
||||
professional_bio String? @db.Text
|
||||
|
||||
projects Project[]
|
||||
|
||||
@@map("admin")
|
||||
}
|
||||
|
||||
@@ -47,6 +52,7 @@ model LamaranMagang {
|
||||
created_at DateTime @default(now())
|
||||
update_at DateTime
|
||||
|
||||
user User?
|
||||
batch_data Batch? @relation(fields: [batch], references: [id], map: "lamaran_magang_batch_foreign")
|
||||
mahasiswa Mahasiswa @relation(fields: [id_mahasiswa], references: [id], map: "lamaran_magang_id_mahasiswa_foreign")
|
||||
lowongan_magang LowonganMagang @relation(fields: [id_lowongan_magang], references: [id], map: "lamaran_magang_id_lowongan_magang_foreign")
|
||||
@@ -95,6 +101,7 @@ model Mahasiswa {
|
||||
motivasi String @db.Text
|
||||
relevant_skills String @db.VarChar(255)
|
||||
|
||||
user User?
|
||||
lamaran_magang LamaranMagang[]
|
||||
|
||||
@@map("mahasiswa")
|
||||
@@ -170,8 +177,107 @@ model Batch {
|
||||
@@map("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
|
||||
|
||||
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")
|
||||
project_members ProjectMember[]
|
||||
task_submissions TaskSubmission[]
|
||||
|
||||
@@index([email])
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Project {
|
||||
id Int @id @default(autoincrement())
|
||||
id_admin Int //mentor project
|
||||
project_icon ProjectIcon @default(code)
|
||||
project_name String @db.VarChar(255)
|
||||
description String @db.Text
|
||||
start_date DateTime @db.Date
|
||||
end_date DateTime @db.Date
|
||||
max_members Int @default(8)
|
||||
status ProjectStatus @default(active)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
admin Admin @relation(fields: [id_admin], references: [id], map: "project_id_admin_foreign")
|
||||
members ProjectMember[]
|
||||
tasks Task[]
|
||||
|
||||
@@index([id_admin], map: "project_id_admin_foreign")
|
||||
@@index([status])
|
||||
@@map("project")
|
||||
}
|
||||
|
||||
model ProjectMember {
|
||||
id Int @id @default(autoincrement())
|
||||
id_project Int
|
||||
id_user Int
|
||||
status ProjectMemberStatus @default(active)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
project Project @relation(fields: [id_project], references: [id], map: "project_member_id_project_foreign")
|
||||
user User @relation(fields: [id_user], references: [id], map: "project_member_id_user_foreign")
|
||||
|
||||
@@unique([id_project, id_user])
|
||||
@@index([id_project], map: "project_member_id_project_foreign")
|
||||
@@index([id_user], map: "project_member_id_user_foreign")
|
||||
@@index([status])
|
||||
@@map("project_member")
|
||||
}
|
||||
|
||||
model Task {
|
||||
id Int @id @default(autoincrement())
|
||||
id_project Int
|
||||
title String @db.VarChar(255)
|
||||
description String @db.Text
|
||||
deadline_at DateTime
|
||||
submission_type SubmissionType
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
project Project @relation(fields: [id_project], references: [id], map: "task_id_project_foreign")
|
||||
submissions TaskSubmission[]
|
||||
|
||||
@@index([id_project], map: "task_id_project_foreign")
|
||||
@@index([deadline_at])
|
||||
@@map("task")
|
||||
}
|
||||
|
||||
model TaskSubmission {
|
||||
id Int @id @default(autoincrement())
|
||||
id_task Int
|
||||
id_user Int
|
||||
file_path String? @db.Text
|
||||
url_link String? @db.Text
|
||||
submitted_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
task Task @relation(fields: [id_task], references: [id], map: "task_submission_id_task_foreign")
|
||||
user User @relation(fields: [id_user], references: [id], map: "task_submission_id_user_foreign")
|
||||
|
||||
@@unique([id_task, id_user])
|
||||
@@index([id_task], map: "task_submission_id_task_foreign")
|
||||
@@index([id_user], map: "task_submission_id_user_foreign")
|
||||
@@map("task_submission")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
admin
|
||||
intern
|
||||
}
|
||||
|
||||
enum StudentRole {
|
||||
@@ -193,3 +299,32 @@ enum PaidStatus {
|
||||
paid
|
||||
unpaid
|
||||
}
|
||||
|
||||
enum ProjectIcon {
|
||||
code
|
||||
chart
|
||||
cloud
|
||||
mobile
|
||||
gear
|
||||
users
|
||||
clipboard
|
||||
speedometer
|
||||
lightbulb
|
||||
shield
|
||||
}
|
||||
|
||||
enum ProjectStatus {
|
||||
active
|
||||
completed
|
||||
archived
|
||||
}
|
||||
|
||||
enum ProjectMemberStatus {
|
||||
active
|
||||
removed
|
||||
}
|
||||
|
||||
enum SubmissionType {
|
||||
file_upload
|
||||
url_link
|
||||
}
|
||||
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const { PrismaClient } = require('../src/generated/prisma');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Diperlukan ketika isi databasenya masih kosong dan ingin menambahkan dummy data sebagai testing development [Rafi 08/06/2026 16:34]
|
||||
async function main() {
|
||||
await prisma.taskSubmission.deleteMany({});
|
||||
await prisma.projectMember.deleteMany({});
|
||||
await prisma.user.deleteMany({});
|
||||
await prisma.task.deleteMany({});
|
||||
await prisma.lamaranMagang.deleteMany({});
|
||||
await prisma.project.deleteMany({});
|
||||
await prisma.lowonganMagang.deleteMany({});
|
||||
await prisma.mahasiswa.deleteMany({});
|
||||
await prisma.riwayatPelamar.deleteMany({});
|
||||
await prisma.feedback.deleteMany({});
|
||||
await prisma.faq.deleteMany({});
|
||||
await prisma.partnership.deleteMany({});
|
||||
await prisma.hasilResearch.deleteMany({});
|
||||
await prisma.batch.deleteMany({});
|
||||
await prisma.admin.deleteMany({});
|
||||
|
||||
const hashedPassword = await bcrypt.hash('password123', 10);
|
||||
|
||||
const admin1 = await prisma.admin.create({
|
||||
data: {
|
||||
nama_depan: 'Admin',
|
||||
nama_belakang: 'One',
|
||||
email: 'admin1@internify.com',
|
||||
password: hashedPassword,
|
||||
role: 'admin',
|
||||
signature: `Admin-One-${Date.now()}`,
|
||||
profile_picture: 'https://placehold.co/150',
|
||||
professional_bio: 'Senior Program Manager at Internify.'
|
||||
}
|
||||
});
|
||||
const admin2 = await prisma.admin.create({
|
||||
data: {
|
||||
nama_depan: 'Admin',
|
||||
nama_belakang: 'Two',
|
||||
email: 'admin2@internify.com',
|
||||
password: hashedPassword,
|
||||
role: 'admin',
|
||||
signature: `Admin-Two-${Date.now()}`,
|
||||
profile_picture: 'https://placehold.co/150',
|
||||
professional_bio: 'Lead Engineering Mentor at Internify.'
|
||||
}
|
||||
});
|
||||
const admin3 = await prisma.admin.create({
|
||||
data: {
|
||||
nama_depan: 'Admin',
|
||||
nama_belakang: 'Three',
|
||||
email: 'admin3@internify.com',
|
||||
password: hashedPassword,
|
||||
role: 'admin',
|
||||
signature: `Admin-Three-${Date.now()}`,
|
||||
profile_picture: 'https://placehold.co/150',
|
||||
professional_bio: 'Design Mentor at Internify.'
|
||||
}
|
||||
});
|
||||
|
||||
const batch1 = await prisma.batch.create({
|
||||
data: { id: 1, batch_number: 1, is_active: false }
|
||||
});
|
||||
const batch2 = await prisma.batch.create({
|
||||
data: { id: 2, batch_number: 2, is_active: false }
|
||||
});
|
||||
const batch3 = await prisma.batch.create({
|
||||
data: { id: 3, batch_number: 3, is_active: true }
|
||||
});
|
||||
|
||||
await prisma.hasilResearch.createMany({
|
||||
data: [
|
||||
{
|
||||
nama_project: 'AI Intern Matcher',
|
||||
deskripsi: 'Match students using machine learning algorithms',
|
||||
image_path: 'https://placehold.co/600x400',
|
||||
link_project: 'https://github.com/internify/ai-matcher'
|
||||
},
|
||||
{
|
||||
nama_project: 'Feedback Sentiment Analyzer',
|
||||
deskripsi: 'Analyze student feedback sentiment trends',
|
||||
image_path: 'https://placehold.co/600x400',
|
||||
link_project: 'https://github.com/internify/sentiment'
|
||||
},
|
||||
{
|
||||
nama_project: 'LMS Portal Engine',
|
||||
deskripsi: 'Modular learning management system backend',
|
||||
image_path: 'https://placehold.co/600x400',
|
||||
link_project: 'https://github.com/internify/lms'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await prisma.partnership.createMany({
|
||||
data: [
|
||||
{ nama_partner: 'Google Indonesia', image_path: 'https://placehold.co/150' },
|
||||
{ nama_partner: 'Microsoft Indonesia', image_path: 'https://placehold.co/150' },
|
||||
{ nama_partner: 'Meta APAC', image_path: 'https://placehold.co/150' }
|
||||
]
|
||||
});
|
||||
|
||||
// 5. Faq
|
||||
await prisma.faq.createMany({
|
||||
data: [
|
||||
{ pertanyaan: 'Bagaimana cara mendaftar magang?', jawaban: 'Buka menu Lowongan Magang, pilih salah satu lowongan aktif, dan klik tombol daftar.' },
|
||||
{ pertanyaan: 'Apakah program magang ini berbayar?', jawaban: 'Tergantung pada tipe lowongan (paid/unpaid). Detail tertera di tiap deskripsi lowongan.' },
|
||||
{ pertanyaan: 'Berapa lama durasi program magang?', jawaban: 'Biasanya durasi magang berlangsung antara 3 hingga 6 bulan penuh.' }
|
||||
]
|
||||
});
|
||||
|
||||
// 6. Feedback
|
||||
await prisma.feedback.createMany({
|
||||
data: [
|
||||
{ nama: 'Budi Utomo', universitas: 'Institut Teknologi Bandung', pesan: 'Pengalaman magang yang luar biasa! Mentornya ramah dan proyeknya menantang.', batch: 1, posisi: 'Web Developer', tahun: 2025, image_path: 'https://placehold.co/150' },
|
||||
{ nama: 'Siti Aminah', universitas: 'Universitas Indonesia', pesan: 'Mendapatkan banyak wawasan praktis tentang UI/UX prototyping di industri nyata.', batch: 2, posisi: 'UI/UX Designer', tahun: 2025, image_path: 'https://placehold.co/150' },
|
||||
{ nama: 'Andi Wijaya', universitas: 'Universitas Gadjah Mada', pesan: 'Sangat direkomendasikan bagi mahasiswa yang ingin belajar data engineering dengan benar.', batch: 2, posisi: 'Data Analyst', tahun: 2026, image_path: 'https://placehold.co/150' }
|
||||
]
|
||||
});
|
||||
|
||||
// 7. RiwayatPelamar
|
||||
await prisma.riwayatPelamar.createMany({
|
||||
data: [
|
||||
{ batch: 1, negara: 'Indonesia', posisi: 'Web Developer', tahun: 2025, jumlah: 100, pelamar_diterima: 10, pelamar_ditolak: 70, pelamar_diproses: 20, presentase_diterima: 10.0, presentase_ditolak: 70.0, presentase_diproses: 20.0 },
|
||||
{ batch: 2, negara: 'Indonesia', posisi: 'UI/UX Designer', tahun: 2025, jumlah: 150, pelamar_diterima: 15, pelamar_ditolak: 120, pelamar_diproses: 15, presentase_diterima: 10.0, presentase_ditolak: 80.0, presentase_diproses: 10.0 },
|
||||
{ batch: 2, negara: 'Malaysia', posisi: 'Data Analyst', tahun: 2026, jumlah: 50, pelamar_diterima: 5, pelamar_ditolak: 40, pelamar_diproses: 5, presentase_diterima: 10.0, presentase_ditolak: 80.0, presentase_diproses: 10.0 }
|
||||
]
|
||||
});
|
||||
|
||||
// 8. Mahasiswa
|
||||
const mhs1 = await prisma.mahasiswa.create({
|
||||
data: { nama_depan: 'Rafi', nama_belakang: 'Athallah', email: 'rafi@student.com', kontak: '08123456789', jurusan: 'Teknik Informatika', universitas: 'ITS', negara: 'Indonesia', cv_path: 'https://example.com/cv-rafi.pdf', portofolio_path: 'https://example.com/portfolio-rafi.pdf', motivasi: 'Ingin mendalami Node.js', relevant_skills: 'Node.js, React, MySQL' }
|
||||
});
|
||||
const mhs2 = await prisma.mahasiswa.create({
|
||||
data: { nama_depan: 'Alice', nama_belakang: 'Smith', email: 'alice@student.com', kontak: '08234567890', jurusan: 'Sistem Informasi', universitas: 'UI', negara: 'Indonesia', cv_path: 'https://example.com/cv-alice.pdf', portofolio_path: 'https://example.com/portfolio-alice.pdf', motivasi: 'Tertarik dengan UI/UX design', relevant_skills: 'Figma, CSS, React' }
|
||||
});
|
||||
const mhs3 = await prisma.mahasiswa.create({
|
||||
data: { nama_depan: 'Bob', nama_belakang: 'Johnson', email: 'bob@student.com', kontak: '08345678901', jurusan: 'Ilmu Komputer', universitas: 'UGM', negara: 'Indonesia', cv_path: 'https://example.com/cv-bob.pdf', portofolio_path: 'https://example.com/portfolio-bob.pdf', motivasi: 'Ingin berkarir di data science', relevant_skills: 'Python, SQL, R' }
|
||||
});
|
||||
|
||||
// 9. LowonganMagang
|
||||
const lowongan1 = await prisma.lowonganMagang.create({
|
||||
data: { id: 'LOW-001', posisi: 'Web Developer', kelompok_peminatan: 'Software Engineering', image_path: 'https://placehold.co/600x400', jobdesk: 'Build REST APIs and user interfaces', lokasi: 'Remote', kualifikasi: 'Knowledge of Node.js & React', benefit: 'Certificate, stipend', durasi_awal: new Date('2026-07-01'), durasi_akhir: new Date('2026-12-31'), id_batch: batch3.id, status_lowongan: 'dibuka', paid: 'paid' }
|
||||
});
|
||||
const lowongan2 = await prisma.lowonganMagang.create({
|
||||
data: { id: 'LOW-002', posisi: 'UI/UX Designer', kelompok_peminatan: 'Design', image_path: 'https://placehold.co/600x400', jobdesk: 'Create wireframes, mockups, and high-fidelity designs', lokasi: 'Hybrid', kualifikasi: 'Proficiency in Figma', benefit: 'Certificate, mentoring', durasi_awal: new Date('2026-07-01'), durasi_akhir: new Date('2026-12-31'), id_batch: batch3.id, status_lowongan: 'dibuka', paid: 'unpaid' }
|
||||
});
|
||||
const lowongan3 = await prisma.lowonganMagang.create({
|
||||
data: { id: 'LOW-003', posisi: 'Data Analyst', kelompok_peminatan: 'Data Science', image_path: 'https://placehold.co/600x400', jobdesk: 'Analyze dataset and create interactive dashboard', lokasi: 'Onsite', kualifikasi: 'Skills in Python, SQL, Tableau', benefit: 'Certificate, meals', durasi_awal: new Date('2026-07-01'), durasi_akhir: new Date('2026-12-31'), id_batch: batch3.id, status_lowongan: 'dibuka', paid: 'paid' }
|
||||
});
|
||||
|
||||
// 10. Project
|
||||
const proj1 = await prisma.project.create({
|
||||
data: { id_admin: admin1.id, project_icon: 'code', project_name: 'Internify Platform Dev', description: 'Upgrading the core Internify platform backend and web client.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), max_members: 5, status: 'active' }
|
||||
});
|
||||
const proj2 = await prisma.project.create({
|
||||
data: { id_admin: admin2.id, project_icon: 'shield', project_name: 'Auth Gateway Module', description: 'Securing API gateway with unified OAuth2 authentication.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), max_members: 3, status: 'active' }
|
||||
});
|
||||
const proj3 = await prisma.project.create({
|
||||
data: { id_admin: admin1.id, project_icon: 'chart', project_name: 'Recruitment Analytics Dashboard', description: 'Developing analytical charts for recruitment metrics.', start_date: new Date('2026-07-15'), end_date: new Date('2026-10-15'), max_members: 4, status: 'active' }
|
||||
});
|
||||
|
||||
// 11. LamaranMagang
|
||||
const lamaran1 = await prisma.lamaranMagang.create({
|
||||
data: { id_mahasiswa: mhs1.id, id_lowongan_magang: lowongan1.id, status: 'diterima', batch: batch3.id, update_at: new Date() }
|
||||
});
|
||||
const lamaran2 = await prisma.lamaranMagang.create({
|
||||
data: { id_mahasiswa: mhs2.id, id_lowongan_magang: lowongan2.id, status: 'diterima', batch: batch3.id, update_at: new Date() }
|
||||
});
|
||||
const lamaran3 = await prisma.lamaranMagang.create({
|
||||
data: { id_mahasiswa: mhs3.id, id_lowongan_magang: lowongan3.id, status: 'diterima', batch: batch3.id, update_at: new Date() }
|
||||
});
|
||||
|
||||
// 12. Task
|
||||
const task1 = await prisma.task.create({
|
||||
data: { id_project: proj1.id, title: 'Database Setup', description: 'Setup database schema and write initial seed script.', deadline_at: new Date('2026-08-01'), submission_type: 'url_link' }
|
||||
});
|
||||
const task2 = await prisma.task.create({
|
||||
data: { id_project: proj1.id, title: 'API Specification', description: 'Draft comprehensive swagger API document.', deadline_at: new Date('2026-08-15'), submission_type: 'file_upload' }
|
||||
});
|
||||
const task3 = await prisma.task.create({
|
||||
data: { id_project: proj2.id, title: 'Implement JWT Validation', description: 'Setup middleware and keys for JWT validation.', deadline_at: new Date('2026-08-10'), submission_type: 'url_link' }
|
||||
});
|
||||
|
||||
// 13. User
|
||||
const user1 = await prisma.user.create({
|
||||
data: { id_mahasiswa: mhs1.id, id_lamaran_magang: lamaran1.id, full_name: 'Rafi Athallah', email: 'rafi@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
||||
});
|
||||
const user2 = await prisma.user.create({
|
||||
data: { id_mahasiswa: mhs2.id, id_lamaran_magang: lamaran2.id, full_name: 'Alice Smith', email: 'alice@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
||||
});
|
||||
const user3 = await prisma.user.create({
|
||||
data: { id_mahasiswa: mhs3.id, id_lamaran_magang: lamaran3.id, full_name: 'Bob Johnson', email: 'bob@student.com', password: hashedPassword, role: 'intern', is_active: true }
|
||||
});
|
||||
|
||||
// 14. ProjectMember
|
||||
await prisma.projectMember.createMany({
|
||||
data: [
|
||||
{ id_project: proj1.id, id_user: user1.id, status: 'active' },
|
||||
{ id_project: proj1.id, id_user: user2.id, status: 'active' },
|
||||
{ id_project: proj2.id, id_user: user3.id, status: 'active' }
|
||||
]
|
||||
});
|
||||
|
||||
// 15. TaskSubmission
|
||||
await prisma.taskSubmission.createMany({
|
||||
data: [
|
||||
{ id_task: task1.id, id_user: user1.id, url_link: 'https://github.com/rafiathallah3' },
|
||||
{ id_task: task2.id, id_user: user2.id, file_path: 'https://example.com/submission-alice.zip' },
|
||||
{ id_task: task3.id, id_user: user3.id, url_link: 'https://github.com/bob/auth-gateway' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('Error seeding data:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -63,6 +63,10 @@ const swaggerOptions = {
|
||||
{
|
||||
name: "Feedback",
|
||||
description: "Internship feedback and testimonial management endpoints"
|
||||
},
|
||||
{
|
||||
name: "Project",
|
||||
description: "Internify LMS project management endpoints"
|
||||
}
|
||||
],
|
||||
components: {
|
||||
|
||||
+83
-78
@@ -1,78 +1,83 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const adminRoutes = require('./routes/admin.routes');
|
||||
const authRoutes = require('./routes/auth.routes');
|
||||
const lowonganMagangRoutes = require('./routes/lowonganMagang.routes');
|
||||
const lamaranMagangRoutes = require('./routes/lamaranMagang.routes');
|
||||
const mahasiswaRoutes = require('./routes/mahasiswa.routes');
|
||||
const partnershipRoutes = require('./routes/partnership.routes');
|
||||
const hasilResearchRoutes = require('./routes/hasilResearch.routes');
|
||||
const faqRoutes = require('./routes/faq.routes');
|
||||
const feedbackRoutes = require('./routes/feedback.routes');
|
||||
const batchRoutes = require('./routes/batch.routes');
|
||||
const setupSwaggerDocs = require('./docs/swagger');
|
||||
|
||||
const PORT = process.env.PORT;
|
||||
const app = express();
|
||||
|
||||
const corsOptions = {
|
||||
origin: true,
|
||||
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
|
||||
credentials: true,
|
||||
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"],
|
||||
exposedHeaders: ["Content-Range", "X-Content-Range"],
|
||||
optionsSuccessStatus: 200,
|
||||
};
|
||||
|
||||
const uploadDir = process.env.NODE_ENV === 'production'
|
||||
? '/tmp/uploads'
|
||||
: path.join(__dirname, 'uploads');
|
||||
|
||||
app.options('*', cors(corsOptions));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
app.use("/uploads", express.static(uploadDir), (req, res, next) => {
|
||||
if (!res.headersSent) {
|
||||
res.status(404).json({
|
||||
status: false,
|
||||
message: `File not found: ${req.path}`,
|
||||
code: 404
|
||||
});
|
||||
}
|
||||
});
|
||||
app.use("/admin-api", adminRoutes);
|
||||
app.use("/auth-api", authRoutes);
|
||||
app.use("/lowongan-magang-api", lowonganMagangRoutes);
|
||||
app.use("/lamaran-magang-api", lamaranMagangRoutes);
|
||||
app.use("/mahasiswa-api", mahasiswaRoutes);
|
||||
app.use("/partnership-api", partnershipRoutes);
|
||||
app.use("/hasil-research-api", hasilResearchRoutes);
|
||||
app.use("/faq-api", faqRoutes);
|
||||
app.use("/feedback-api", feedbackRoutes);
|
||||
app.use("/batch-api", batchRoutes);
|
||||
|
||||
setupSwaggerDocs(app);
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
status: false,
|
||||
message: `Route not found: ${req.method} ${req.path}`,
|
||||
code: 404
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`listening at http://localhost:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
|
||||
/*
|
||||
NOTE : saran saya untuk penamaan variabel untuk setiap response dibuat konsisten menjadi inggris, tolong komunikasi aja ke fe 😁
|
||||
*/
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const adminRoutes = require('./routes/admin.routes');
|
||||
const authRoutes = require('./routes/auth.routes');
|
||||
const lowonganMagangRoutes = require('./routes/lowonganMagang.routes');
|
||||
const lamaranMagangRoutes = require('./routes/lamaranMagang.routes');
|
||||
const mahasiswaRoutes = require('./routes/mahasiswa.routes');
|
||||
const partnershipRoutes = require('./routes/partnership.routes');
|
||||
const hasilResearchRoutes = require('./routes/hasilResearch.routes');
|
||||
const faqRoutes = require('./routes/faq.routes');
|
||||
const feedbackRoutes = require('./routes/feedback.routes');
|
||||
const batchRoutes = require('./routes/batch.routes');
|
||||
const projectRoutes = require('./routes/project.routes');
|
||||
const setupSwaggerDocs = require('./docs/swagger');
|
||||
|
||||
const PORT = process.env.PORT;
|
||||
const app = express();
|
||||
|
||||
const corsOptions = {
|
||||
origin: true,
|
||||
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
|
||||
credentials: true,
|
||||
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"],
|
||||
exposedHeaders: ["Content-Range", "X-Content-Range"],
|
||||
optionsSuccessStatus: 200,
|
||||
};
|
||||
|
||||
const uploadDir = process.env.NODE_ENV === 'production'
|
||||
? '/tmp/uploads'
|
||||
: path.join(__dirname, 'uploads');
|
||||
|
||||
app.options('*', cors(corsOptions));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
app.use("/uploads", express.static(uploadDir), (req, res, next) => {
|
||||
if (!res.headersSent) {
|
||||
res.status(404).json({
|
||||
status: false,
|
||||
message: `File not found: ${req.path}`,
|
||||
code: 404
|
||||
});
|
||||
}
|
||||
});
|
||||
app.use("/admin-api", adminRoutes);
|
||||
app.use("/auth-api", authRoutes);
|
||||
app.use("/lowongan-magang-api", lowonganMagangRoutes);
|
||||
app.use("/lamaran-magang-api", lamaranMagangRoutes);
|
||||
app.use("/mahasiswa-api", mahasiswaRoutes);
|
||||
app.use("/partnership-api", partnershipRoutes);
|
||||
app.use("/hasil-research-api", hasilResearchRoutes);
|
||||
app.use("/faq-api", faqRoutes);
|
||||
app.use("/feedback-api", feedbackRoutes);
|
||||
app.use("/batch-api", batchRoutes);
|
||||
app.use("/project-api", projectRoutes);
|
||||
// app.use("/project-member-api", projectMemberRoutes);
|
||||
// app.use("/task-api", taskRoutes);
|
||||
|
||||
|
||||
setupSwaggerDocs(app);
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
status: false,
|
||||
message: `Route not found: ${req.method} ${req.path}`,
|
||||
code: 404
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`listening at http://localhost:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
|
||||
/*
|
||||
NOTE : saran saya untuk penamaan variabel untuk setiap response dibuat konsisten menjadi inggris, tolong komunikasi aja ke fe 😁
|
||||
*/
|
||||
|
||||
@@ -7,5 +7,5 @@ module.exports = function isAdmin(req, res, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return response(res, 'fail', new UnauthorizedError('Akses ditolak: Hanya admin yang dapat mengakses ini'), 'Akses ditolak: Hanya admin yang dapat mengakses ini', ERROR.UNAUTHORIZED);
|
||||
return response(res, 'fail', { err: new UnauthorizedError('Akses ditolak: Hanya admin yang dapat mengakses ini') }, 'Akses ditolak: Hanya admin yang dapat mengakses ini', ERROR.UNAUTHORIZED);
|
||||
}
|
||||
@@ -3,7 +3,9 @@ const { response, data, error } = require('../../../helpers/utils/wrapper');
|
||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||
const { BadRequestError, UnauthorizedError, InternalServerError } = require('../../../helpers/error');
|
||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||
const { loginModel } = require('../models/auth.model');
|
||||
const { loginModel, updateProfileModel } = require('../models/auth.model');
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
|
||||
|
||||
class AuthController {
|
||||
async login(req, res) {
|
||||
@@ -48,7 +50,7 @@ class AuthController {
|
||||
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
|
||||
}
|
||||
|
||||
const result = await authService.getCurrentUser(id);
|
||||
const result = await authService.getCurrentUser(id, req.role);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
@@ -67,6 +69,48 @@ class AuthController {
|
||||
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(req, res) {
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, updateProfileModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
if (req.file?.filename) {
|
||||
await deleteFileIfExists(req.file.filename).catch(() => { });
|
||||
}
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Data tidak valid',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const id = req.id;
|
||||
const role = req.role;
|
||||
|
||||
if (!id || !role) {
|
||||
if (req.file?.filename) {
|
||||
await deleteFileIfExists(req.file.filename).catch(() => { });
|
||||
}
|
||||
return response(res, 'fail', error(new UnauthorizedError('Tidak diotorisasi: ID pengguna tidak ditemukan')));
|
||||
}
|
||||
|
||||
const result = await authService.updateProfile(id, role, validatePayload.data, req.file);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Profil berhasil diperbarui', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
if (req.file?.filename) {
|
||||
await deleteFileIfExists(req.file.filename).catch(() => { });
|
||||
}
|
||||
return response(res, 'fail', error(new InternalServerError(err.message)), 'Terjadi kesalahan yang tidak terduga', ERROR.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthController();
|
||||
|
||||
@@ -66,9 +66,28 @@ const resetPasswordModel = joi.object().keys({
|
||||
}),
|
||||
});
|
||||
|
||||
const updateProfileModel = joi.object().keys({
|
||||
full_name: joi.string().max(255).optional().messages({
|
||||
'string.base': 'Nama lengkap harus berupa teks.',
|
||||
'string.max': 'Nama lengkap tidak boleh lebih dari 255 karakter.'
|
||||
}),
|
||||
email: joi.string().email({ tlds: { allow: false } }).optional().messages({
|
||||
'string.base': 'Email harus berupa teks.',
|
||||
'string.email': 'Format email tidak valid.'
|
||||
}),
|
||||
password: joi.string().min(8).optional().messages({
|
||||
'string.base': 'Password harus berupa teks.',
|
||||
'string.min': 'Password minimal {#limit} karakter.'
|
||||
}),
|
||||
professional_bio: joi.string().allow('', null).optional().messages({
|
||||
'string.base': 'Bio harus berupa teks.'
|
||||
})
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
loginModel,
|
||||
registerModel,
|
||||
forgotPasswordModel,
|
||||
resetPasswordModel,
|
||||
updateProfileModel,
|
||||
};
|
||||
|
||||
@@ -2,58 +2,133 @@ const bcrypt = require('bcrypt');
|
||||
const adminService = require('../../admin/services/admin.service');
|
||||
const { createToken } = require('../../../middleware/verifyJWT');
|
||||
const { data, error } = require('../../../helpers/utils/wrapper');
|
||||
const { UnauthorizedError, NotFoundError } = require('../../../helpers/error');
|
||||
const { UnauthorizedError, NotFoundError, ForbiddenError, ConflictError, BadRequestError } = require('../../../helpers/error');
|
||||
const prisma = require('../../../helpers/db/db_connection');
|
||||
|
||||
class AuthService {
|
||||
async login(email, password) {
|
||||
try {
|
||||
const result = await adminService.getAdminByEmail(email);
|
||||
if (result.err || !result.data) {
|
||||
let userRecord = null;
|
||||
let isUserAdmin = false;
|
||||
|
||||
const adminResult = await adminService.getAdminByEmail(email);
|
||||
if (adminResult.data) {
|
||||
userRecord = adminResult.data;
|
||||
isUserAdmin = true;
|
||||
} else {
|
||||
const internRecord = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: email,
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
if (internRecord) {
|
||||
userRecord = internRecord;
|
||||
isUserAdmin = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userRecord) {
|
||||
return error(new UnauthorizedError('Email atau password tidak valid'));
|
||||
}
|
||||
|
||||
const admin = result.data;
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, admin.password);
|
||||
const isPasswordValid = await bcrypt.compare(password, userRecord.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return error(new UnauthorizedError('Email atau password tidak valid'));
|
||||
}
|
||||
|
||||
const token = createToken({
|
||||
id: admin.id.toString(),
|
||||
email: admin.email,
|
||||
role: admin.role,
|
||||
signature: admin.signature
|
||||
});
|
||||
const role = isUserAdmin ? (userRecord.role || 'admin') : 'intern';
|
||||
|
||||
return data({
|
||||
token,
|
||||
admin: {
|
||||
id: admin.id.toString(),
|
||||
nama_depan: admin.nama_depan,
|
||||
nama_belakang: admin.nama_belakang,
|
||||
email: admin.email,
|
||||
role: admin.role,
|
||||
signature: admin.signature,
|
||||
},
|
||||
});
|
||||
const tokenPayload = {
|
||||
id: userRecord.id.toString(),
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
signature: isUserAdmin ? userRecord.signature : null
|
||||
};
|
||||
|
||||
const token = createToken(tokenPayload);
|
||||
|
||||
if (isUserAdmin) {
|
||||
return data({
|
||||
token,
|
||||
user: {
|
||||
id: userRecord.id.toString(),
|
||||
full_name: `${userRecord.nama_depan}${userRecord.nama_belakang ? ' ' + userRecord.nama_belakang : ''}`,
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
signature: userRecord.signature
|
||||
},
|
||||
admin: {
|
||||
id: userRecord.id.toString(),
|
||||
nama_depan: userRecord.nama_depan,
|
||||
nama_belakang: userRecord.nama_belakang,
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
signature: userRecord.signature,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return data({
|
||||
token,
|
||||
user: {
|
||||
id: userRecord.id.toString(),
|
||||
full_name: userRecord.full_name,
|
||||
email: userRecord.email,
|
||||
role: role,
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentUser(userId) {
|
||||
async getCurrentUser(userId, role) {
|
||||
try {
|
||||
const result = await adminService.getAdminById(userId);
|
||||
if (role === 'admin') {
|
||||
const result = await adminService.getAdminById(userId);
|
||||
|
||||
if (result.err) {
|
||||
return result;
|
||||
if (result.err) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { password, ...adminWithoutPassword } = result.data;
|
||||
|
||||
return data({
|
||||
...adminWithoutPassword,
|
||||
role: 'admin'
|
||||
});
|
||||
} else if (role === 'intern') {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: parseInt(userId),
|
||||
is_active: true
|
||||
},
|
||||
include: {
|
||||
lamaran_magang: {
|
||||
include: {
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
kelompok_peminatan: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return error(new NotFoundError('Pengguna tidak ditemukan'));
|
||||
}
|
||||
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
|
||||
return data(userWithoutPassword);
|
||||
} else {
|
||||
return error(new NotFoundError('Role tidak valid'));
|
||||
}
|
||||
|
||||
const { password, ...adminWithoutPassword } = result.data;
|
||||
|
||||
return data(adminWithoutPassword);
|
||||
} catch (err) {
|
||||
return error(new NotFoundError('Pengguna tidak ditemukan'));
|
||||
}
|
||||
@@ -67,6 +142,146 @@ class AuthService {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(userId, role, payload, imageFile) {
|
||||
try {
|
||||
const { full_name, email, password, professional_bio } = payload;
|
||||
|
||||
if (email) {
|
||||
const existingAdmin = await prisma.admin.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
if (existingAdmin && (role !== 'admin' || existingAdmin.id !== parseInt(userId))) {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new ConflictError('Email sudah terdaftar'));
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
if (existingUser && (role !== 'intern' || existingUser.id !== parseInt(userId))) {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new ConflictError('Email sudah terdaftar'));
|
||||
}
|
||||
}
|
||||
|
||||
let hashedPassword = undefined;
|
||||
if (password) {
|
||||
hashedPassword = await bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
if (role === 'admin') {
|
||||
const adminId = parseInt(userId);
|
||||
const existingAdmin = await prisma.admin.findUnique({
|
||||
where: { id: adminId }
|
||||
});
|
||||
if (!existingAdmin) {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new NotFoundError('Admin tidak ditemukan'));
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (full_name) {
|
||||
const nameParts = full_name.trim().split(/\s+/);
|
||||
updateData.nama_depan = nameParts[0];
|
||||
updateData.nama_belakang = nameParts.slice(1).join(' ') || '';
|
||||
}
|
||||
if (email) updateData.email = email;
|
||||
if (hashedPassword) updateData.password = hashedPassword;
|
||||
if (professional_bio !== undefined) updateData.professional_bio = professional_bio;
|
||||
|
||||
if (imageFile) {
|
||||
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
|
||||
const imagePath = imageUploadHelper.uploadImage(imageFile);
|
||||
updateData.profile_picture = imagePath;
|
||||
|
||||
if (existingAdmin.profile_picture) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
const path = require('path');
|
||||
const oldFileName = path.basename(existingAdmin.profile_picture);
|
||||
await deleteFileIfExists(oldFileName).catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedAdmin = await prisma.admin.update({
|
||||
where: { id: adminId },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
const { password: _, ...adminWithoutPassword } = updatedAdmin;
|
||||
return data({
|
||||
...adminWithoutPassword,
|
||||
role: 'admin'
|
||||
});
|
||||
|
||||
} else if (role === 'intern') {
|
||||
if (imageFile) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
return error(new ForbiddenError('Hanya admin yang dapat mengupload foto profil'));
|
||||
}
|
||||
|
||||
const internId = parseInt(userId);
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { id: internId }
|
||||
});
|
||||
if (!existingUser) {
|
||||
return error(new NotFoundError('User tidak ditemukan'));
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (full_name) updateData.full_name = full_name;
|
||||
if (email) updateData.email = email;
|
||||
if (hashedPassword) updateData.password = hashedPassword;
|
||||
if (professional_bio !== undefined) updateData.professional_bio = professional_bio;
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: internId },
|
||||
data: updateData,
|
||||
include: {
|
||||
lamaran_magang: {
|
||||
include: {
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
kelompok_peminatan: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { password: _, ...userWithoutPassword } = updatedUser;
|
||||
return data(userWithoutPassword);
|
||||
} else {
|
||||
if (imageFile?.filename) {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename).catch(() => { });
|
||||
}
|
||||
return error(new NotFoundError('Role tidak valid'));
|
||||
}
|
||||
} catch (err) {
|
||||
if (imageFile?.filename) {
|
||||
try {
|
||||
const { deleteFileIfExists } = require('../../../helpers/utils/fileHelper');
|
||||
await deleteFileIfExists(imageFile.filename);
|
||||
} catch (cleanupErr) {
|
||||
console.error('Failed to cleanup file:', cleanupErr);
|
||||
}
|
||||
}
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthService();
|
||||
|
||||
@@ -43,7 +43,7 @@ const sendConfirmationEmail = async (mahasiswa, lowongan) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendStatusEmail = async (lamaran, status) => {
|
||||
const sendStatusEmail = async (lamaran, status, generatedPassword = null) => {
|
||||
try {
|
||||
const { mahasiswa, lowongan_magang } = lamaran;
|
||||
const { nama_depan, nama_belakang, email } = mahasiswa;
|
||||
@@ -61,6 +61,14 @@ const sendStatusEmail = async (lamaran, status) => {
|
||||
<p>Halo ${fullName},</p>
|
||||
<p>Selamat! Lamaran Anda untuk posisi <strong>${posisi}</strong> di <strong>Humic Engineering</strong> telah diterima.</p>
|
||||
<p>Tim kami sangat terkesan dengan profil dan kualifikasi Anda. Kami akan segera menghubungi Anda terkait tahapan selanjutnya.</p>
|
||||
${generatedPassword ? `
|
||||
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 5px; margin: 15px 0; border: 1px solid #e0e0e0; font-family: monospace;">
|
||||
<p style="margin-top: 0; font-weight: bold; color: #333; font-family: sans-serif;">Akun Portal Intern Anda telah dibuat:</p>
|
||||
<p style="margin: 5px 0;"><strong>Email:</strong> ${email}</p>
|
||||
<p style="margin: 5px 0;"><strong>Password:</strong> ${generatedPassword}</p>
|
||||
<p style="margin-bottom: 0; font-size: 13px; color: #555; font-family: sans-serif;">Silakan masuk menggunakan akun ini untuk melihat project dan tugas magang Anda.</p>
|
||||
</div>
|
||||
` : ''}
|
||||
<p>Terima kasih telah melamar dan kami menantikan kerja sama yang luar biasa bersama Anda.</p>
|
||||
`
|
||||
: `
|
||||
|
||||
@@ -2,6 +2,9 @@ const lamaranMagangRepository = require('../repositories/lamaranMagang.repositor
|
||||
const mahasiswaRepository = require('../../mahasiswa/repositories/mahasiswa.repository');
|
||||
const lowonganMagangRepository = require('../../lowonganMagang/repositories/lowonganMagang.repository');
|
||||
const batchRepository = require('../../batch/repositories/batch.repository');
|
||||
const bcrypt = require('bcrypt');
|
||||
const prisma = require('../../../helpers/db/db_connection');
|
||||
const crypto = require('crypto');
|
||||
const imageUploadHelper = require('../../../helpers/utils/imageUpload.helper');
|
||||
const { sendConfirmationEmail, sendStatusEmail } = require('../helpers/email.helper');
|
||||
const ExcelJS = require('exceljs');
|
||||
@@ -151,7 +154,40 @@ class LamaranMagangService {
|
||||
|
||||
const result = await lamaranMagangRepository.updateStatus(id_lamaran_magang, status);
|
||||
|
||||
await sendStatusEmail(checkResult.data, status);
|
||||
let generatedPassword = null;
|
||||
if (status === 'diterima') {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id_mahasiswa: checkResult.data.id_mahasiswa },
|
||||
{ id_lamaran_magang: parseInt(id_lamaran_magang) },
|
||||
{ email: checkResult.data.mahasiswa.email }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Akan dibuat akunnya ketika sudah ACC ama mentor dengan password yang random [Rafi 08/06/2026 16:05]
|
||||
if (!existingUser) {
|
||||
generatedPassword = crypto.randomBytes(4).toString('hex');
|
||||
const hashedPassword = await bcrypt.hash(generatedPassword, 10);
|
||||
|
||||
const fullName = `${checkResult.data.mahasiswa.nama_depan}${checkResult.data.mahasiswa.nama_belakang ? ' ' + checkResult.data.mahasiswa.nama_belakang : ''}`;
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id_mahasiswa: checkResult.data.id_mahasiswa,
|
||||
id_lamaran_magang: parseInt(id_lamaran_magang),
|
||||
full_name: fullName,
|
||||
email: checkResult.data.mahasiswa.email,
|
||||
password: hashedPassword,
|
||||
role: 'intern',
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await sendStatusEmail(checkResult.data, status, generatedPassword);
|
||||
|
||||
return data(result);
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
const projectService = require('../services/project.service');
|
||||
const { response, error } = require('../../../helpers/utils/wrapper');
|
||||
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||
const { InternalServerError } = require('../../../helpers/error');
|
||||
const { isValidPayload } = require('../../../helpers/utils/validator');
|
||||
const {
|
||||
createProjectModel,
|
||||
updateProjectModel,
|
||||
getAllProjectsModel,
|
||||
} = require('../models/project.model');
|
||||
|
||||
const getActor = (req) => ({
|
||||
id: req.id,
|
||||
role: req.role,
|
||||
email: req.email,
|
||||
});
|
||||
|
||||
class ProjectController {
|
||||
async createProject(req, res) {
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.body, createProjectModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Invalid project data',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.createProject(validatePayload.data, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Project created successfully', SUCCESS.CREATED);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllProjects(req, res) {
|
||||
try {
|
||||
const validatePayload = isValidPayload(req.query, getAllProjectsModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Invalid query data',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getAllProjects(validatePayload.data, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Projects retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const actor = getActor(req);
|
||||
|
||||
const result = await projectService.getProjectById(id, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Project detail retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProject(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const validatePayload = isValidPayload(req.body, updateProjectModel);
|
||||
|
||||
if (validatePayload.err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
{ err: validatePayload.err, data: null },
|
||||
'Invalid project data',
|
||||
ERROR.EXPECTATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.updateProject(id, validatePayload.data, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Project updated successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async archiveProject(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const actor = getActor(req);
|
||||
|
||||
const result = await projectService.archiveProject(id, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Project archived successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyProjects(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getMyProjects(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Intern projects retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyTasks(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getMyTasks(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Intern tasks retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getMentorProjects(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getMentorProjects(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Mentor projects retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getInterns(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.getInterns(actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Interns retrieved successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async assignMember(req, res) {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await projectService.assignMember(req.body, actor);
|
||||
|
||||
if (result.err) {
|
||||
return response(res, 'fail', result);
|
||||
}
|
||||
|
||||
return response(res, 'success', result, 'Member assigned successfully', SUCCESS.OK);
|
||||
} catch (err) {
|
||||
return response(
|
||||
res,
|
||||
'fail',
|
||||
error(new InternalServerError(err.message)),
|
||||
'Unexpected error occurred',
|
||||
ERROR.INTERNAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProjectController();
|
||||
@@ -0,0 +1,9 @@
|
||||
const projectController = require("./controllers/project.controller");
|
||||
const projectRepository = require("./repositories/project.repository");
|
||||
const projectService = require("./services/project.service");
|
||||
|
||||
module.exports = {
|
||||
projectController,
|
||||
projectService,
|
||||
projectRepository
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
const joi = require('joi');
|
||||
|
||||
const PROJECT_ICON_VALUES = [
|
||||
'code',
|
||||
'chart',
|
||||
'cloud',
|
||||
'mobile',
|
||||
'gear',
|
||||
'users',
|
||||
'clipboard',
|
||||
'speedometer',
|
||||
'lightbulb',
|
||||
'shield',
|
||||
];
|
||||
|
||||
const PROJECT_STATUS_VALUES = [
|
||||
'active',
|
||||
'completed',
|
||||
'archived',
|
||||
];
|
||||
|
||||
const createProjectModel = joi.object().keys({
|
||||
project_icon: joi.string().valid(...PROJECT_ICON_VALUES).required().messages({
|
||||
'string.base': 'Project icon must be a string.',
|
||||
'string.empty': 'Project icon cannot be empty.',
|
||||
'any.only': `Project icon must be one of: ${PROJECT_ICON_VALUES.join(', ')}.`,
|
||||
'any.required': 'Project icon is required.',
|
||||
}),
|
||||
|
||||
project_name: joi.string().max(255).required().messages({
|
||||
'string.base': 'Project name must be a string.',
|
||||
'string.empty': 'Project name cannot be empty.',
|
||||
'string.max': 'Project name must not exceed {#limit} characters.',
|
||||
'any.required': 'Project name is required.',
|
||||
}),
|
||||
|
||||
description: joi.string().optional().allow('', null).messages({
|
||||
'string.base': 'Project description must be a string.',
|
||||
}),
|
||||
|
||||
start_date: joi.date().required().messages({
|
||||
'date.base': 'Start date must be a valid date.',
|
||||
'any.required': 'Start date is required.',
|
||||
}),
|
||||
|
||||
end_date: joi.date().min(joi.ref('start_date')).required().messages({
|
||||
'date.base': 'End date must be a valid date.',
|
||||
'date.min': 'End date must be greater than or equal to start date.',
|
||||
'any.required': 'End date is required.',
|
||||
}),
|
||||
|
||||
member_emails: joi.array().items(
|
||||
joi.string().email({ tlds: { allow: false } }).messages({
|
||||
'string.email': 'Member email format is invalid.',
|
||||
})
|
||||
).optional().default([]).messages({
|
||||
'array.base': 'Member emails must be an array.',
|
||||
}),
|
||||
});
|
||||
|
||||
const updateProjectModel = joi.object().keys({
|
||||
project_icon: joi.string().valid(...PROJECT_ICON_VALUES).optional().messages({
|
||||
'string.base': 'Project icon must be a string.',
|
||||
'any.only': `Project icon must be one of: ${PROJECT_ICON_VALUES.join(', ')}.`,
|
||||
}),
|
||||
|
||||
project_name: joi.string().max(255).optional().messages({
|
||||
'string.base': 'Project name must be a string.',
|
||||
'string.max': 'Project name must not exceed {#limit} characters.',
|
||||
}),
|
||||
|
||||
description: joi.string().optional().allow('', null).messages({
|
||||
'string.base': 'Project description must be a string.',
|
||||
}),
|
||||
|
||||
start_date: joi.date().optional().messages({
|
||||
'date.base': 'Start date must be a valid date.',
|
||||
}),
|
||||
|
||||
end_date: joi.date().optional().messages({
|
||||
'date.base': 'End date must be a valid date.',
|
||||
}),
|
||||
|
||||
status: joi.string().valid(...PROJECT_STATUS_VALUES).optional().messages({
|
||||
'string.base': 'Project status must be a string.',
|
||||
'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`,
|
||||
}),
|
||||
});
|
||||
|
||||
const getAllProjectsModel = joi.object().keys({
|
||||
status: joi.string().valid(...PROJECT_STATUS_VALUES).optional().messages({
|
||||
'string.base': 'Project status must be a string.',
|
||||
'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`,
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createProjectModel,
|
||||
updateProjectModel,
|
||||
getAllProjectsModel,
|
||||
};
|
||||
@@ -0,0 +1,395 @@
|
||||
const prisma = require("../../../helpers/db/db_connection");
|
||||
|
||||
class ProjectRepository {
|
||||
async findAdminById(id) {
|
||||
return prisma.admin.findUnique({
|
||||
where: {
|
||||
id: parseInt(id),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findUsersByEmails(emails) {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
email: {
|
||||
in: emails,
|
||||
},
|
||||
is_active: true,
|
||||
},
|
||||
include: {
|
||||
lamaran_magang: {
|
||||
include: {
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
kelompok_peminatan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createWithMembers(projectData, userIds = []) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const project = await tx.project.create({
|
||||
data: projectData,
|
||||
});
|
||||
|
||||
if (userIds.length > 0) {
|
||||
await tx.projectMember.createMany({
|
||||
data: userIds.map((id_user) => ({
|
||||
id_project: project.id,
|
||||
id_user,
|
||||
status: "active",
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
return tx.project.findUnique({
|
||||
where: {
|
||||
id: project.id,
|
||||
},
|
||||
include: this.projectDetailInclude(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async findAll({ status, actor }) {
|
||||
const where = {};
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (actor?.role === "intern") {
|
||||
where.members = {
|
||||
some: {
|
||||
id_user: parseInt(actor.id),
|
||||
status: "active",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return prisma.project.findMany({
|
||||
where,
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
nama_depan: true,
|
||||
nama_belakang: true,
|
||||
email: true,
|
||||
profile_picture: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return prisma.project.findUnique({
|
||||
where: {
|
||||
id: parseInt(id),
|
||||
},
|
||||
include: this.projectDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async updateById(id, projectData) {
|
||||
return prisma.project.update({
|
||||
where: {
|
||||
id: parseInt(id),
|
||||
},
|
||||
data: projectData,
|
||||
include: this.projectDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async archiveById(id) {
|
||||
return prisma.project.update({
|
||||
where: {
|
||||
id: parseInt(id),
|
||||
},
|
||||
data: {
|
||||
status: "archived",
|
||||
},
|
||||
include: this.projectDetailInclude(),
|
||||
});
|
||||
}
|
||||
|
||||
async checkActiveMember(idProject, idUser) {
|
||||
return prisma.projectMember.findFirst({
|
||||
where: {
|
||||
id_project: parseInt(idProject),
|
||||
id_user: parseInt(idUser),
|
||||
status: "active",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
projectDetailInclude() {
|
||||
return {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
nama_depan: true,
|
||||
nama_belakang: true,
|
||||
email: true,
|
||||
profile_picture: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
full_name: true,
|
||||
email: true,
|
||||
professional_bio: true,
|
||||
is_active: true,
|
||||
lamaran_magang: {
|
||||
select: {
|
||||
id: true,
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
kelompok_peminatan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
deadline_at: true,
|
||||
submission_type: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
},
|
||||
orderBy: {
|
||||
deadline_at: "asc",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async findProjectsByIntern(internId) {
|
||||
return prisma.project.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
id_user: parseInt(internId),
|
||||
status: "active",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
nama_depan: true,
|
||||
nama_belakang: true,
|
||||
email: true,
|
||||
profile_picture: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findTasksByIntern(internId) {
|
||||
return prisma.task.findMany({
|
||||
where: {
|
||||
project: {
|
||||
members: {
|
||||
some: {
|
||||
id_user: parseInt(internId),
|
||||
status: "active",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
deadline_at: "asc",
|
||||
},
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
project_icon: true,
|
||||
},
|
||||
},
|
||||
submissions: {
|
||||
where: {
|
||||
id_user: parseInt(internId),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
file_path: true,
|
||||
url_link: true,
|
||||
submitted_at: true,
|
||||
updated_at: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findProjectsByMentor(adminId) {
|
||||
return prisma.project.findMany({
|
||||
where: {
|
||||
id_admin: parseInt(adminId),
|
||||
},
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
include: {
|
||||
admin: {
|
||||
select: {
|
||||
id: true,
|
||||
nama_depan: true,
|
||||
nama_belakang: true,
|
||||
email: true,
|
||||
profile_picture: true,
|
||||
professional_bio: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findActiveInternsWithProjects() {
|
||||
return prisma.user.findMany({
|
||||
where: {
|
||||
role: "intern",
|
||||
is_active: true,
|
||||
},
|
||||
include: {
|
||||
project_members: {
|
||||
where: {
|
||||
status: "active",
|
||||
},
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
project_name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lamaran_magang: {
|
||||
include: {
|
||||
lowongan_magang: {
|
||||
select: {
|
||||
posisi: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async assignMember(id_project, id_user) {
|
||||
return prisma.projectMember.create({
|
||||
data: {
|
||||
id_project: parseInt(id_project),
|
||||
id_user: parseInt(id_user),
|
||||
status: "active",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findUserByIdAndActive(id) {
|
||||
return prisma.user.findFirst({
|
||||
where: {
|
||||
id: parseInt(id),
|
||||
is_active: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findMembership(idProject, idUser) {
|
||||
return prisma.projectMember.findFirst({
|
||||
where: {
|
||||
id_project: parseInt(idProject),
|
||||
id_user: parseInt(idUser),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateMembershipStatus(id, status) {
|
||||
return prisma.projectMember.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProjectRepository();
|
||||
@@ -0,0 +1,514 @@
|
||||
const projectRepository = require("../repositories/project.repository");
|
||||
const { data, error } = require("../../../helpers/utils/wrapper");
|
||||
const {
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
ForbiddenError,
|
||||
ConflictError,
|
||||
} = require("../../../helpers/error");
|
||||
|
||||
class ProjectService {
|
||||
async createProject(projectPayload, actor) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin can create project"),
|
||||
);
|
||||
}
|
||||
|
||||
const { member_emails, ...projectData } = projectPayload;
|
||||
|
||||
const admin = await projectRepository.findAdminById(actor.id);
|
||||
|
||||
if (!admin) {
|
||||
return error(new NotFoundError("Admin not found"));
|
||||
}
|
||||
|
||||
const normalizedEmails = this.normalizeEmails(member_emails || []);
|
||||
|
||||
if (normalizedEmails.length > 8) {
|
||||
return error(
|
||||
new BadRequestError(
|
||||
"Project member limit exceeded. Maximum member is 8",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const users =
|
||||
normalizedEmails.length > 0
|
||||
? await projectRepository.findUsersByEmails(normalizedEmails)
|
||||
: [];
|
||||
|
||||
if (normalizedEmails.length > 0) {
|
||||
const foundEmails = users.map((user) => user.email.toLowerCase());
|
||||
const missingEmails = normalizedEmails.filter(
|
||||
(email) => !foundEmails.includes(email),
|
||||
);
|
||||
|
||||
if (missingEmails.length > 0) {
|
||||
return error(
|
||||
new NotFoundError(
|
||||
`User with these emails not found: ${missingEmails.join(", ")}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newProjectData = {
|
||||
id_admin: parseInt(actor.id),
|
||||
project_icon: projectData.project_icon,
|
||||
project_name: projectData.project_name,
|
||||
description: projectData.description || "",
|
||||
start_date: projectData.start_date,
|
||||
end_date: projectData.end_date,
|
||||
max_members: 8,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
const userIds = users.map((user) => user.id);
|
||||
const project = await projectRepository.createWithMembers(
|
||||
newProjectData,
|
||||
userIds,
|
||||
);
|
||||
|
||||
return data(this.mapProjectDetail(project));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllProjects(query = {}, actor = {}) {
|
||||
try {
|
||||
const projects = await projectRepository.findAll({
|
||||
status: query.status,
|
||||
actor,
|
||||
});
|
||||
|
||||
const mappedProjects = projects.map((project) => ({
|
||||
id: project.id,
|
||||
project_icon: project.project_icon,
|
||||
project_name: project.project_name,
|
||||
description: project.description,
|
||||
start_date: project.start_date,
|
||||
end_date: project.end_date,
|
||||
max_members: project.max_members,
|
||||
status: project.status,
|
||||
admin: this.mapAdmin(project.admin),
|
||||
total_members: project.members?.length || 0,
|
||||
total_tasks: project.tasks?.length || 0,
|
||||
created_at: project.created_at,
|
||||
updated_at: project.updated_at,
|
||||
}));
|
||||
|
||||
return data(mappedProjects);
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectById(id, actor = {}) {
|
||||
try {
|
||||
const project = await projectRepository.findById(id);
|
||||
|
||||
if (!project) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
const accessError = await this.validateProjectAccess(project.id, actor);
|
||||
if (accessError) {
|
||||
return error(accessError);
|
||||
}
|
||||
|
||||
return data(this.mapProjectDetail(project));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async updateProject(id, updatePayload, actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin can update project"),
|
||||
);
|
||||
}
|
||||
|
||||
const existingProject = await projectRepository.findById(id);
|
||||
|
||||
if (!existingProject) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
if (Object.keys(updatePayload).length === 0) {
|
||||
return error(
|
||||
new BadRequestError(
|
||||
"At least one field is required to update project",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const updateData = { ...updatePayload };
|
||||
|
||||
if (updateData.start_date || updateData.end_date) {
|
||||
const startDate = updateData.start_date || existingProject.start_date;
|
||||
const endDate = updateData.end_date || existingProject.end_date;
|
||||
|
||||
if (new Date(endDate) < new Date(startDate)) {
|
||||
return error(
|
||||
new BadRequestError(
|
||||
"End date must be greater than or equal to start date",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.description === null) {
|
||||
updateData.description = "";
|
||||
}
|
||||
|
||||
const updatedProject = await projectRepository.updateById(id, updateData);
|
||||
|
||||
return data(this.mapProjectDetail(updatedProject));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async archiveProject(id, actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin can archive project"),
|
||||
);
|
||||
}
|
||||
|
||||
const existingProject = await projectRepository.findById(id);
|
||||
|
||||
if (!existingProject) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
if (existingProject.status === "archived") {
|
||||
return data(this.mapProjectDetail(existingProject));
|
||||
}
|
||||
|
||||
const archivedProject = await projectRepository.archiveById(id);
|
||||
|
||||
return data(this.mapProjectDetail(archivedProject));
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async validateProjectAccess(idProject, actor = {}) {
|
||||
if (!actor?.id || !actor?.role) {
|
||||
return new ForbiddenError(
|
||||
"Access denied: authentication data is missing",
|
||||
);
|
||||
}
|
||||
|
||||
if (actor.role === "admin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (actor.role === "intern") {
|
||||
const membership = await projectRepository.checkActiveMember(
|
||||
idProject,
|
||||
actor.id,
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
return new ForbiddenError(
|
||||
"Access denied: you are not a member of this project",
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ForbiddenError("Access denied: invalid role");
|
||||
}
|
||||
|
||||
normalizeEmails(emails = []) {
|
||||
return [
|
||||
...new Set(
|
||||
emails.filter(Boolean).map((email) => email.trim().toLowerCase()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
mapProjectDetail(project) {
|
||||
if (!project) return null;
|
||||
|
||||
return {
|
||||
id: project.id,
|
||||
project_icon: project.project_icon,
|
||||
project_name: project.project_name,
|
||||
description: project.description,
|
||||
start_date: project.start_date,
|
||||
end_date: project.end_date,
|
||||
max_members: project.max_members,
|
||||
status: project.status,
|
||||
admin: this.mapAdmin(project.admin),
|
||||
members:
|
||||
project.members?.map((member) => this.mapProjectMember(member)) || [],
|
||||
tasks: project.tasks || [],
|
||||
total_members: project.members?.length || 0,
|
||||
total_tasks: project.tasks?.length || 0,
|
||||
created_at: project.created_at,
|
||||
updated_at: project.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
mapAdmin(admin) {
|
||||
if (!admin) return null;
|
||||
|
||||
return {
|
||||
id: admin.id,
|
||||
full_name: `${admin.nama_depan}${admin.nama_belakang ? ` ${admin.nama_belakang}` : ""}`,
|
||||
email: admin.email,
|
||||
profile_picture: admin.profile_picture || null,
|
||||
professional_bio: admin.professional_bio || null,
|
||||
};
|
||||
}
|
||||
|
||||
mapProjectMember(member) {
|
||||
if (!member) return null;
|
||||
|
||||
const user = member.user;
|
||||
const lowongan = user?.lamaran_magang?.lowongan_magang;
|
||||
|
||||
return {
|
||||
id: member.id,
|
||||
id_user: member.id_user,
|
||||
status: member.status,
|
||||
user: {
|
||||
id: user.id,
|
||||
full_name: user.full_name,
|
||||
email: user.email,
|
||||
professional_bio: user.professional_bio || null,
|
||||
position: lowongan?.posisi || null,
|
||||
kelompok_peminatan: lowongan?.kelompok_peminatan || null,
|
||||
},
|
||||
created_at: member.created_at,
|
||||
updated_at: member.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
async getMyProjects(actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("User ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "intern") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only interns can view their projects"),
|
||||
);
|
||||
}
|
||||
|
||||
const projects = await projectRepository.findProjectsByIntern(actor.id);
|
||||
|
||||
const mappedProjects = projects.map((project) => ({
|
||||
id: project.id,
|
||||
project_icon: project.project_icon,
|
||||
project_name: project.project_name,
|
||||
description: project.description,
|
||||
start_date: project.start_date,
|
||||
end_date: project.end_date,
|
||||
max_members: project.max_members,
|
||||
status: project.status,
|
||||
admin: this.mapAdmin(project.admin),
|
||||
total_members: project.members?.length || 0,
|
||||
total_tasks: project.tasks?.length || 0,
|
||||
created_at: project.created_at,
|
||||
updated_at: project.updated_at,
|
||||
}));
|
||||
|
||||
return data(mappedProjects);
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getMyTasks(actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("User ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "intern") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only interns can view their tasks"),
|
||||
);
|
||||
}
|
||||
|
||||
const tasks = await projectRepository.findTasksByIntern(actor.id);
|
||||
|
||||
const mappedTasks = tasks.map((task) => {
|
||||
const submission = task.submissions?.[0] || null;
|
||||
return {
|
||||
id: task.id,
|
||||
id_project: task.id_project,
|
||||
project_name: task.project?.project_name || null,
|
||||
project_icon: task.project?.project_icon || null,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
deadline_at: task.deadline_at,
|
||||
submission_type: task.submission_type,
|
||||
created_at: task.created_at,
|
||||
updated_at: task.updated_at,
|
||||
submission_status: submission ? "submitted" : "not_submitted",
|
||||
submission_details: submission ? {
|
||||
id: submission.id,
|
||||
file_path: submission.file_path,
|
||||
url_link: submission.url_link,
|
||||
submitted_at: submission.submitted_at,
|
||||
updated_at: submission.updated_at,
|
||||
} : null,
|
||||
};
|
||||
});
|
||||
|
||||
return data(mappedTasks);
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getMentorProjects(actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only mentors/admins can view their created projects"),
|
||||
);
|
||||
}
|
||||
|
||||
const projects = await projectRepository.findProjectsByMentor(actor.id);
|
||||
|
||||
const mappedProjects = projects.map((project) => ({
|
||||
id: project.id,
|
||||
project_icon: project.project_icon,
|
||||
project_name: project.project_name,
|
||||
description: project.description,
|
||||
start_date: project.start_date,
|
||||
end_date: project.end_date,
|
||||
max_members: project.max_members,
|
||||
status: project.status,
|
||||
admin: this.mapAdmin(project.admin),
|
||||
total_members: project.members?.length || 0,
|
||||
total_tasks: project.tasks?.length || 0,
|
||||
created_at: project.created_at,
|
||||
updated_at: project.updated_at,
|
||||
}));
|
||||
|
||||
return data(mappedProjects);
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getInterns(actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only admin/mentor can access interns list"),
|
||||
);
|
||||
}
|
||||
|
||||
const interns = await projectRepository.findActiveInternsWithProjects();
|
||||
|
||||
const mappedInterns = interns.map((intern) => {
|
||||
const activeMembership = intern.project_members?.[0] || null;
|
||||
const projectName = activeMembership?.project?.project_name || "Unassigned";
|
||||
const role = intern.lamaran_magang?.lowongan_magang?.posisi || "-";
|
||||
|
||||
return {
|
||||
id: intern.id,
|
||||
name: intern.full_name,
|
||||
email: intern.email,
|
||||
projectName: projectName,
|
||||
role: role,
|
||||
isAssignedByMentor: projectName !== "Unassigned",
|
||||
avatar: null
|
||||
};
|
||||
});
|
||||
|
||||
return data(mappedInterns);
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async assignMember(payload, actor = {}) {
|
||||
try {
|
||||
if (!actor?.id) {
|
||||
return error(new BadRequestError("Admin ID is required"));
|
||||
}
|
||||
|
||||
if (actor.role !== "admin") {
|
||||
return error(
|
||||
new ForbiddenError("Access denied: only mentors/admins can assign members"),
|
||||
);
|
||||
}
|
||||
|
||||
const { id_project, id_user } = payload;
|
||||
|
||||
if (!id_project || !id_user) {
|
||||
return error(new BadRequestError("Project ID and User ID are required"));
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const project = await projectRepository.findById(id_project);
|
||||
if (!project) {
|
||||
return error(new NotFoundError("Project not found"));
|
||||
}
|
||||
|
||||
// Check if user exists and is active
|
||||
const user = await projectRepository.findUserByIdAndActive(id_user);
|
||||
if (!user) {
|
||||
return error(new NotFoundError("User not found or inactive"));
|
||||
}
|
||||
|
||||
// Check if already a member of this project
|
||||
const existingMembership = await projectRepository.findMembership(id_project, id_user);
|
||||
|
||||
let membership;
|
||||
if (existingMembership) {
|
||||
if (existingMembership.status === 'active') {
|
||||
return error(new ConflictError("User is already an active member of this project"));
|
||||
}
|
||||
// Reactivate membership
|
||||
membership = await projectRepository.updateMembershipStatus(existingMembership.id, 'active');
|
||||
} else {
|
||||
// Create new membership
|
||||
membership = await projectRepository.assignMember(id_project, id_user);
|
||||
}
|
||||
|
||||
return data(membership);
|
||||
} catch (err) {
|
||||
return error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProjectService();
|
||||
@@ -1,6 +1,8 @@
|
||||
const express = require('express');
|
||||
const { authController } = require('../modules/auth');
|
||||
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||
const multer = require('../middleware/multer');
|
||||
const { multerErrorHandler } = require('../middleware/multer');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -92,4 +94,52 @@ router.post("/login", authController.login);
|
||||
*/
|
||||
router.get("/me", verifyJWT, authController.me);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /auth-api/update-profile:
|
||||
* patch:
|
||||
* summary: Update authenticated user profile
|
||||
* description: Update profile details (full name, email, password, professional bio). Admins can also upload a profile picture.
|
||||
* tags: [Auth]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* full_name:
|
||||
* type: string
|
||||
* example: "Jonathan Kristina"
|
||||
* email:
|
||||
* type: string
|
||||
* example: "jonathan.kristina@example.com"
|
||||
* password:
|
||||
* type: string
|
||||
* example: "newSecurePassword123"
|
||||
* professional_bio:
|
||||
* type: string
|
||||
* example: "Experienced UI/UX Designer and Engineer"
|
||||
* profile_picture:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Admin only. Optional profile picture upload.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Profile successfully updated
|
||||
* 400:
|
||||
* description: Bad Request (Validation error)
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Interns uploading images)
|
||||
* 409:
|
||||
* description: Conflict (Email already in use)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.patch('/update-profile', verifyJWT, multer.single('profile_picture'), multerErrorHandler, authController.updateProfile);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,439 @@
|
||||
const express = require('express');
|
||||
const { projectController } = require('../modules/project');
|
||||
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||
const isAdmin = require('../middleware/isAdmin');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Project:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* example: 1
|
||||
* project_icon:
|
||||
* type: string
|
||||
* enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield]
|
||||
* example: code
|
||||
* project_name:
|
||||
* type: string
|
||||
* example: Internify LMS
|
||||
* description:
|
||||
* type: string
|
||||
* example: Project for developing Internify LMS features
|
||||
* start_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: 2026-06-10
|
||||
* end_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: 2026-08-10
|
||||
* max_members:
|
||||
* type: integer
|
||||
* example: 8
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, completed, archived]
|
||||
* example: active
|
||||
* total_members:
|
||||
* type: integer
|
||||
* example: 5
|
||||
* total_tasks:
|
||||
* type: integer
|
||||
* example: 3
|
||||
* ProjectCreateRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - project_icon
|
||||
* - project_name
|
||||
* - start_date
|
||||
* - end_date
|
||||
* properties:
|
||||
* project_icon:
|
||||
* type: string
|
||||
* enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield]
|
||||
* example: code
|
||||
* project_name:
|
||||
* type: string
|
||||
* example: Internify LMS
|
||||
* description:
|
||||
* type: string
|
||||
* example: Project for developing Internify LMS features
|
||||
* start_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: 2026-06-10
|
||||
* end_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: 2026-08-10
|
||||
* member_emails:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: email
|
||||
* example: ["intern1@example.com", "intern2@example.com"]
|
||||
* ProjectUpdateRequest:
|
||||
* type: object
|
||||
* properties:
|
||||
* project_icon:
|
||||
* type: string
|
||||
* enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield]
|
||||
* example: cloud
|
||||
* project_name:
|
||||
* type: string
|
||||
* example: Internify LMS Updated
|
||||
* description:
|
||||
* type: string
|
||||
* example: Updated project description
|
||||
* start_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: 2026-06-15
|
||||
* end_date:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: 2026-08-20
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, completed, archived]
|
||||
* example: completed
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/add:
|
||||
* post:
|
||||
* summary: Create a new project
|
||||
* description: Create a new Internify LMS project. Admin can optionally invite interns by email.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ProjectCreateRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Project created successfully
|
||||
* 400:
|
||||
* description: Bad request
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden
|
||||
* 404:
|
||||
* description: Admin or invited user not found
|
||||
* 417:
|
||||
* description: Validation error
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.post('/add', verifyJWT, isAdmin, projectController.createProject);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/get:
|
||||
* get:
|
||||
* summary: Get all projects
|
||||
* description: Admin can see all projects. Intern can only see projects where they are active members.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, completed, archived]
|
||||
* required: false
|
||||
* description: Filter projects by status
|
||||
* example: active
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Projects retrieved successfully
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 417:
|
||||
* description: Validation error
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/get', verifyJWT, projectController.getAllProjects);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/get/{id}:
|
||||
* get:
|
||||
* summary: Get project detail
|
||||
* description: Admin can access any project. Intern can only access project where they are active members.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Project ID
|
||||
* example: 1
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Project detail retrieved successfully
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden
|
||||
* 404:
|
||||
* description: Project not found
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/get/:id', verifyJWT, projectController.getProjectById);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/update/{id}:
|
||||
* patch:
|
||||
* summary: Update project
|
||||
* description: Update project information. Only admin can access this endpoint.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Project ID
|
||||
* example: 1
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ProjectUpdateRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Project updated successfully
|
||||
* 400:
|
||||
* description: Bad request
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden
|
||||
* 404:
|
||||
* description: Project not found
|
||||
* 417:
|
||||
* description: Validation error
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.patch('/update/:id', verifyJWT, isAdmin, projectController.updateProject);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/delete/{id}:
|
||||
* delete:
|
||||
* summary: Archive project
|
||||
* description: Archive project by changing its status to archived. This endpoint does not hard delete the project.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Project ID
|
||||
* example: 1
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Project archived successfully
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden
|
||||
* 404:
|
||||
* description: Project not found
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.delete('/delete/:id', verifyJWT, isAdmin, projectController.archiveProject);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/my-projects:
|
||||
* get:
|
||||
* summary: Get intern's active projects
|
||||
* description: Retrieve all active projects where the logged-in intern is a member.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Intern projects retrieved successfully
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Only for interns)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/my-projects', verifyJWT, projectController.getMyProjects);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/my-tasks:
|
||||
* get:
|
||||
* summary: Get intern's tasks and submission statuses
|
||||
* description: Retrieve all tasks from all projects where the logged-in intern is an active member, including their specific submission detail/status.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Intern tasks retrieved successfully
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Only for interns)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/my-tasks', verifyJWT, projectController.getMyTasks);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/mentor-projects:
|
||||
* get:
|
||||
* summary: Get projects created by mentor
|
||||
* description: Retrieve all projects created by the logged-in admin/mentor.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Mentor projects retrieved successfully
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Only for mentors)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/mentor-projects', verifyJWT, isAdmin, projectController.getMentorProjects);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/interns:
|
||||
* get:
|
||||
* summary: Get all interns details
|
||||
* description: Retrieve details for all active interns, including their current assigned project and project role. Accessible only by admin/mentor.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Interns details retrieved successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: "success"
|
||||
* message:
|
||||
* type: string
|
||||
* example: "Interns retrieved successfully"
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* example: 1
|
||||
* name:
|
||||
* type: string
|
||||
* example: "Rafi Athallah"
|
||||
* email:
|
||||
* type: string
|
||||
* example: "rafi@student.com"
|
||||
* projectName:
|
||||
* type: string
|
||||
* example: "Internify Platform Dev"
|
||||
* role:
|
||||
* type: string
|
||||
* example: "Web Developer"
|
||||
* isAssignedByMentor:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* avatar:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: null
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Only for admin/mentors)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.get('/interns', verifyJWT, isAdmin, projectController.getInterns);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /project-api/assign-member:
|
||||
* post:
|
||||
* summary: Assign an intern to a project
|
||||
* description: Assigns an intern to a project by project ID and user ID. Only accessible by mentor/admin.
|
||||
* tags: [Project]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - id_project
|
||||
* - id_user
|
||||
* properties:
|
||||
* id_project:
|
||||
* type: integer
|
||||
* example: 1
|
||||
* id_user:
|
||||
* type: integer
|
||||
* example: 2
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Member assigned successfully
|
||||
* 400:
|
||||
* description: Bad request
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 403:
|
||||
* description: Forbidden (Only for admin/mentors)
|
||||
* 404:
|
||||
* description: Project or User not found
|
||||
* 409:
|
||||
* description: Conflict (User is already an active member of this project)
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.post('/assign-member', verifyJWT, isAdmin, projectController.assignMember);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user