feat: ngebuat API tambahan dan upload middleware
This commit is contained in:
+9
-1
@@ -1,3 +1,4 @@
|
||||
import path from 'path';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
@@ -8,10 +9,17 @@ import { errorHandler, CustomError } from './middleware/errorHandler';
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
app.use(morgan('dev'));
|
||||
|
||||
// File upload statis biar bisa diakses lewat /uploads atau /api/uploads
|
||||
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||
app.use('/api/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (_req, file, cb) => {
|
||||
let folder = 'uploads/';
|
||||
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
|
||||
folder += 'docs/';
|
||||
} else {
|
||||
folder += 'images/';
|
||||
}
|
||||
|
||||
fs.mkdirSync(folder, { recursive: true });
|
||||
cb(null, folder);
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (_req: any, file: any, cb: any) => {
|
||||
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
|
||||
if (file.mimetype === 'application/pdf') {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('CV dan Portofolio harus dalam format PDF!'), false);
|
||||
}
|
||||
} else {
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('File upload harus berupa gambar!'), false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // Maksimal 10MB yaa, nanti paling bisa diganti oleh admin melalui website
|
||||
}
|
||||
});
|
||||
+11
-9
@@ -17,7 +17,7 @@ const generateToken = (payload: { id: string; email: string; role: string }) =>
|
||||
|
||||
/**
|
||||
* @route POST /api/auth/register
|
||||
* @desc Mendaftarkan user baru dengan role STUDENT (Mahasiswa)
|
||||
* @desc Daftarin user baru dgn role STUDENT (Mahasiswa)
|
||||
* @access Public
|
||||
*/
|
||||
router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -101,7 +101,7 @@ router.post('/register', async (req: Request, res: Response, next: NextFunction)
|
||||
|
||||
/**
|
||||
* @route POST /api/auth/login
|
||||
* @desc Autentikasi user dan dapatin token
|
||||
* @desc Autentikasi user biar dapet token
|
||||
* @access Public
|
||||
*/
|
||||
router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -145,12 +145,14 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
profile,
|
||||
data: {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
profile,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -160,7 +162,7 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
|
||||
|
||||
/**
|
||||
* @route GET /api/auth/profile
|
||||
* @desc Dapatin profile user yang sedang login
|
||||
* @desc Dapetin profil user yg lagi login
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/profile', protect, async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||
import prisma from '../lib/prisma';
|
||||
import { CustomError } from '../middleware/errorHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Map data faq dari DB ke format frontend
|
||||
function mapToFrontend(faq: any) {
|
||||
return {
|
||||
id: faq.id,
|
||||
pertanyaan: faq.pertanyaan,
|
||||
jawaban: faq.jawaban,
|
||||
created_at: faq.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @route GET /api/faq-api/get
|
||||
* @desc Dapetin semua list FAQ
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const faqs = await prisma.faq.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: faqs.map(mapToFrontend)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/faq-api/get/:id
|
||||
* @desc Dapetin detail satu FAQ
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const faq = await prisma.faq.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!faq) {
|
||||
const error: CustomError = new Error('FAQ tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: mapToFrontend(faq)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/faq-api/add
|
||||
* @desc Tambah FAQ baru (khusus admin ya)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.post('/add', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { pertanyaan, jawaban } = req.body;
|
||||
|
||||
if (!pertanyaan || !jawaban) {
|
||||
const error: CustomError = new Error('Pertanyaan dan jawaban harus diisi');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const faq = await prisma.faq.create({
|
||||
data: {
|
||||
pertanyaan,
|
||||
jawaban
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: 'FAQ berhasil ditambahkan',
|
||||
data: mapToFrontend(faq)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PATCH /api/faq-api/update/:id
|
||||
* @desc Update data FAQ (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { pertanyaan, jawaban } = req.body;
|
||||
|
||||
const existing = await prisma.faq.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('FAQ tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const updated = await prisma.faq.update({
|
||||
where: { id: parseInt(id, 10) },
|
||||
data: {
|
||||
pertanyaan: pertanyaan ?? existing.pertanyaan,
|
||||
jawaban: jawaban ?? existing.jawaban
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'FAQ berhasil diupdate',
|
||||
data: mapToFrontend(updated)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/faq-api/delete/:id
|
||||
* @desc Hapus data FAQ (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const existing = await prisma.faq.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('FAQ tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
await prisma.faq.delete({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'FAQ berhasil dihapus'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,206 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||
import prisma from '../lib/prisma';
|
||||
import { upload } from '../middleware/upload';
|
||||
import { CustomError } from '../middleware/errorHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Map data feedback dari DB ke format frontend
|
||||
function mapToFrontend(feedback: any) {
|
||||
return {
|
||||
id: feedback.id,
|
||||
nama: feedback.nama,
|
||||
universitas: feedback.universitas,
|
||||
posisi: feedback.posisi,
|
||||
batch: feedback.batch,
|
||||
tahun: feedback.tahun,
|
||||
pesan: feedback.pesan,
|
||||
image_path: feedback.imagePath,
|
||||
createdAt: feedback.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @route GET /api/feedback-api/get
|
||||
* @desc Dapetin semua feedback dari mahasiswa
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const feedbacks = await prisma.feedback.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: feedbacks.map(mapToFrontend)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/feedback-api/get/:id
|
||||
* @desc Dapetin detail satu feedback
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const feedback = await prisma.feedback.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!feedback) {
|
||||
const error: CustomError = new Error('Feedback tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: mapToFrontend(feedback)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/feedback-api/add
|
||||
* @desc Tambah feedback baru (bisa admin / student)
|
||||
* @access Private (Admin/Student)
|
||||
*/
|
||||
router.post('/add', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { nama, universitas, posisi, batch, tahun, pesan } = req.body;
|
||||
|
||||
if (!nama || !universitas || !posisi || !batch || !tahun || !pesan) {
|
||||
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const parsedBatch = parseInt(batch, 10);
|
||||
const parsedTahun = parseInt(tahun, 10);
|
||||
|
||||
if (isNaN(parsedBatch) || isNaN(parsedTahun)) {
|
||||
const error: CustomError = new Error('Batch dan tahun harus berupa angka');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
|
||||
|
||||
const feedback = await prisma.feedback.create({
|
||||
data: {
|
||||
nama,
|
||||
universitas,
|
||||
posisi,
|
||||
batch: parsedBatch,
|
||||
tahun: parsedTahun,
|
||||
pesan,
|
||||
imagePath
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: 'Feedback berhasil ditambahkan',
|
||||
data: mapToFrontend(feedback)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PATCH /api/feedback-api/update/:id
|
||||
* @desc Update data feedback (bisa admin / student)
|
||||
* @access Private (Admin/Student)
|
||||
*/
|
||||
router.patch('/update/:id', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { nama, universitas, posisi, batch, tahun, pesan } = req.body;
|
||||
|
||||
const existing = await prisma.feedback.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Feedback tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
let imagePath = existing.imagePath;
|
||||
if (req.file) {
|
||||
imagePath = `/uploads/images/${req.file.filename}`;
|
||||
}
|
||||
|
||||
const parsedBatch = batch ? parseInt(batch, 10) : undefined;
|
||||
const parsedTahun = tahun ? parseInt(tahun, 10) : undefined;
|
||||
|
||||
if ((batch && isNaN(parsedBatch as number)) || (tahun && isNaN(parsedTahun as number))) {
|
||||
const error: CustomError = new Error('Batch dan tahun harus berupa angka');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const updated = await prisma.feedback.update({
|
||||
where: { id: parseInt(id, 10) },
|
||||
data: {
|
||||
nama: nama ?? existing.nama,
|
||||
universitas: universitas ?? existing.universitas,
|
||||
posisi: posisi ?? existing.posisi,
|
||||
batch: parsedBatch ?? existing.batch,
|
||||
tahun: parsedTahun ?? existing.tahun,
|
||||
pesan: pesan ?? existing.pesan,
|
||||
imagePath
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Feedback berhasil diupdate',
|
||||
data: mapToFrontend(updated)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/feedback-api/delete/:id
|
||||
* @desc Hapus feedback (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const existing = await prisma.feedback.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Feedback tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
await prisma.feedback.delete({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Feedback berhasil dihapus'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,10 +1,23 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import prisma from '../lib/prisma';
|
||||
import authRouter from './auth';
|
||||
import lowonganRouter from './lowongan';
|
||||
import lamaranRouter from './lamaran';
|
||||
import partnershipRouter from './partnership';
|
||||
import researchRouter from './research';
|
||||
import feedbackRouter from './feedback';
|
||||
import faqRouter from './faq';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/auth', authRouter);
|
||||
router.use('/auth-api', authRouter);
|
||||
router.use('/lowongan-magang-api', lowonganRouter);
|
||||
router.use('/lamaran-magang-api', lamaranRouter);
|
||||
router.use('/partnership-api', partnershipRouter);
|
||||
router.use('/hasil-research-api', researchRouter);
|
||||
router.use('/feedback-api', feedbackRouter);
|
||||
router.use('/faq-api', faqRouter);
|
||||
|
||||
/**
|
||||
* @route GET /api/health
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||
import prisma from '../lib/prisma';
|
||||
import { upload } from '../middleware/upload';
|
||||
import { CustomError } from '../middleware/errorHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Map data lamaran dari DB ke format frontend
|
||||
function mapToFrontend(lamaran: any) {
|
||||
return {
|
||||
id: lamaran.id,
|
||||
status: (lamaran.status || 'diproses').toLowerCase(),
|
||||
batch: lamaran.batch,
|
||||
createdAt: lamaran.createdAt,
|
||||
updatedAt: lamaran.updatedAt,
|
||||
mahasiswa: lamaran.mahasiswa ? {
|
||||
id: lamaran.mahasiswa.id,
|
||||
nama_depan: lamaran.mahasiswa.namaDepan,
|
||||
nama_belakang: lamaran.mahasiswa.namaBelakang || '',
|
||||
email: lamaran.mahasiswa.user?.email || '',
|
||||
kontak: lamaran.mahasiswa.kontak,
|
||||
jurusan: lamaran.mahasiswa.jurusan,
|
||||
universitas: lamaran.mahasiswa.universitas,
|
||||
negara: lamaran.mahasiswa.negara,
|
||||
cv_path: lamaran.mahasiswa.cvPath,
|
||||
portofolio_path: lamaran.mahasiswa.portofolioPath,
|
||||
motivasi: lamaran.mahasiswa.motivasi,
|
||||
relevant_skills: lamaran.mahasiswa.relevantSkills
|
||||
} : null,
|
||||
lowongan_magang: lamaran.lowonganMagang ? {
|
||||
id: lamaran.lowonganMagang.id,
|
||||
posisi: lamaran.lowonganMagang.posisi,
|
||||
kelompok_peminatan: lamaran.lowonganMagang.kelompokPeminatan
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @route POST /api/lamaran-magang-api/add/:lowonganId
|
||||
* @desc Kirim pendaftaran magang baru
|
||||
* @access Public
|
||||
*/
|
||||
router.post(
|
||||
'/add/:lowonganId',
|
||||
upload.fields([
|
||||
{ name: 'cv', maxCount: 1 },
|
||||
{ name: 'portofolio', maxCount: 1 }
|
||||
]),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { lowonganId } = req.params;
|
||||
const {
|
||||
nama_depan,
|
||||
nama_belakang,
|
||||
email,
|
||||
kontak,
|
||||
universitas,
|
||||
negara,
|
||||
jurusan,
|
||||
batch,
|
||||
motivasi,
|
||||
relevant_skills
|
||||
} = req.body;
|
||||
|
||||
if (!nama_depan || !email || !kontak || !universitas || !negara || !jurusan || !motivasi) {
|
||||
const error: CustomError = new Error('Mohon lengkapi semua field wajib yang diperlukan');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const files = req.files as { [fieldname: string]: Express.Multer.File[] };
|
||||
const cvFile = files?.['cv']?.[0];
|
||||
const portofolioFile = files?.['portofolio']?.[0];
|
||||
|
||||
if (!cvFile || !portofolioFile) {
|
||||
const error: CustomError = new Error('CV dan Portofolio wajib diunggah!');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const lowongan = await prisma.lowonganMagang.findUnique({
|
||||
where: { id: lowonganId }
|
||||
});
|
||||
|
||||
if (!lowongan) {
|
||||
const error: CustomError = new Error('Lowongan magang tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
let user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { mahasiswa: true }
|
||||
});
|
||||
|
||||
const cvPath = `/uploads/docs/${cvFile.filename}`;
|
||||
const portofolioPath = `/uploads/docs/${portofolioFile.filename}`;
|
||||
|
||||
if (!user) {
|
||||
// Kalo user belom terdaftar, kita buatin akun STUDENT baru
|
||||
const randomPassword = Math.random().toString(36).substring(2, 15);
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const passwordHash = await bcrypt.hash(randomPassword, salt);
|
||||
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
passwordHash,
|
||||
role: 'STUDENT',
|
||||
mahasiswa: {
|
||||
create: {
|
||||
namaDepan: nama_depan,
|
||||
namaBelakang: nama_belakang || null,
|
||||
kontak,
|
||||
jurusan,
|
||||
universitas,
|
||||
negara,
|
||||
cvPath,
|
||||
portofolioPath,
|
||||
motivasi,
|
||||
relevantSkills: relevant_skills || ''
|
||||
}
|
||||
}
|
||||
},
|
||||
include: { mahasiswa: true }
|
||||
});
|
||||
} else {
|
||||
// Kalo user udah ada, kita update data profil mahasiswanya aja
|
||||
if (user.mahasiswa) {
|
||||
await prisma.mahasiswa.update({
|
||||
where: { id: user.mahasiswa.id },
|
||||
data: {
|
||||
namaDepan: nama_depan,
|
||||
namaBelakang: nama_belakang || user.mahasiswa.namaBelakang,
|
||||
kontak: kontak || user.mahasiswa.kontak,
|
||||
jurusan: jurusan || user.mahasiswa.jurusan,
|
||||
universitas: universitas || user.mahasiswa.universitas,
|
||||
negara: negara || user.mahasiswa.negara,
|
||||
cvPath,
|
||||
portofolioPath,
|
||||
motivasi,
|
||||
relevantSkills: relevant_skills || user.mahasiswa.relevantSkills
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const parsedBatch = batch ? parseInt(batch, 10) : lowongan.idBatch;
|
||||
|
||||
const lamaran = await prisma.lamaranMagang.create({
|
||||
data: {
|
||||
idMahasiswa: user.mahasiswa!.id,
|
||||
idLowonganMagang: lowongan.id,
|
||||
status: 'DIPROSES',
|
||||
batch: parsedBatch
|
||||
},
|
||||
include: {
|
||||
mahasiswa: {
|
||||
include: { user: true }
|
||||
},
|
||||
lowonganMagang: true
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: 'Pendaftaran magang berhasil dikirim',
|
||||
data: mapToFrontend(lamaran)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/lamaran-magang-api/get
|
||||
* @desc Dapetin semua data pelamar (admin aja ya)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.get('/get', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const applications = await prisma.lamaranMagang.findMany({
|
||||
include: {
|
||||
mahasiswa: {
|
||||
include: { user: true }
|
||||
},
|
||||
lowonganMagang: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: applications.map(mapToFrontend)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/lamaran-magang-api/get/:id
|
||||
* @desc Dapetin detail satu lamaran berdasarkan id
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.get('/get/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const application = await prisma.lamaranMagang.findUnique({
|
||||
where: { id: parseInt(id, 10) },
|
||||
include: {
|
||||
mahasiswa: {
|
||||
include: { user: true }
|
||||
},
|
||||
lowonganMagang: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!application) {
|
||||
const error: CustomError = new Error('Lamaran tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: mapToFrontend(application)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PATCH /api/lamaran-magang-api/update/:id
|
||||
* @desc Ganti status lamaran (diterima, ditolak, dll)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!status) {
|
||||
const error: CustomError = new Error('Mohon masukkan status baru');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const formattedStatus = status.toUpperCase();
|
||||
if (formattedStatus !== 'DITERIMA' && formattedStatus !== 'DITOLAK' && formattedStatus !== 'DIPROSES') {
|
||||
const error: CustomError = new Error('Status tidak valid');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const updated = await prisma.lamaranMagang.update({
|
||||
where: { id: parseInt(id, 10) },
|
||||
data: {
|
||||
status: formattedStatus as any
|
||||
},
|
||||
include: {
|
||||
mahasiswa: {
|
||||
include: { user: true }
|
||||
},
|
||||
lowonganMagang: true
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Status lamaran berhasil diupdate',
|
||||
data: mapToFrontend(updated)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/lamaran-magang-api/delete
|
||||
* @desc Hapus semua data pelamar
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.delete('/delete', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await prisma.lamaranMagang.deleteMany({});
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Seluruh data pelamar berhasil dihapus'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/lamaran-magang-api/export
|
||||
* @desc Ekspor data pelamar ke file Excel (.xlsx)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.get('/export', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const applications = await prisma.lamaranMagang.findMany({
|
||||
include: {
|
||||
mahasiswa: {
|
||||
include: { user: true }
|
||||
},
|
||||
lowonganMagang: true
|
||||
}
|
||||
});
|
||||
|
||||
const rows = applications.map((app) => ({
|
||||
ID: app.id,
|
||||
Nama: `${app.mahasiswa?.namaDepan} ${app.mahasiswa?.namaBelakang || ''}`.trim(),
|
||||
Email: app.mahasiswa?.user?.email || '',
|
||||
Kontak: app.mahasiswa?.kontak || '',
|
||||
Universitas: app.mahasiswa?.universitas || '',
|
||||
Jurusan: app.mahasiswa?.jurusan || '',
|
||||
Negara: app.mahasiswa?.negara || '',
|
||||
Posisi: app.lowonganMagang?.posisi || '',
|
||||
Status: app.status,
|
||||
Motivasi: app.mahasiswa?.motivasi || '',
|
||||
Skills: app.mahasiswa?.relevantSkills || ''
|
||||
}));
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(rows);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Applications');
|
||||
|
||||
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="data.xlsx"');
|
||||
res.send(buffer);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||
import prisma from '../lib/prisma';
|
||||
import { upload } from '../middleware/upload';
|
||||
import { CustomError } from '../middleware/errorHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Nyari batch yg aktif, kalo ga ada ya kita bikinin yg baru
|
||||
async function getOrCreateActiveBatch() {
|
||||
let batch = await prisma.batch.findFirst({
|
||||
where: { isActive: true }
|
||||
});
|
||||
if (!batch) {
|
||||
batch = await prisma.batch.create({
|
||||
data: {
|
||||
batchNumber: 1,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
function mapToFrontend(lowongan: any) {
|
||||
return {
|
||||
id: lowongan.id,
|
||||
id_batch: lowongan.idBatch,
|
||||
posisi: lowongan.posisi,
|
||||
kelompok_peminatan: lowongan.kelompokPeminatan,
|
||||
image_path: lowongan.imagePath,
|
||||
jobdesk: lowongan.jobdesk,
|
||||
lokasi: lowongan.lokasi,
|
||||
kualifikasi: lowongan.kualifikasi,
|
||||
benefit: lowongan.benefit,
|
||||
durasi_awal: lowongan.durasiAwal,
|
||||
durasi_akhir: lowongan.durasiAkhir,
|
||||
status_lowongan: lowongan.statusLowongan,
|
||||
paid: lowongan.paid,
|
||||
createdAt: lowongan.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @route GET /api/lowongan-magang-api/get
|
||||
* @desc Ambil semua daftar lowongan magang nih
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const lowongans = await prisma.lowonganMagang.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: lowongans.map(mapToFrontend)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/lowongan-magang-api/get/kelompok-all
|
||||
* @desc Ambil semua kelompok peminatan yg unik
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get/kelompok-all', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const result = await prisma.lowonganMagang.findMany({
|
||||
select: { kelompokPeminatan: true },
|
||||
distinct: ['kelompokPeminatan']
|
||||
});
|
||||
const categories = result.map((r) => r.kelompokPeminatan);
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: categories
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/lowongan-magang-api/get/id/:id
|
||||
* @desc Ambil detail lowongan magang berdasarkan id
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get/id/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const lowongan = await prisma.lowonganMagang.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
if (!lowongan) {
|
||||
const error: CustomError = new Error('Lowongan tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: mapToFrontend(lowongan)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/lowongan-magang-api/add
|
||||
* @desc Tambah lowongan magang baru (khusus admin ya)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const {
|
||||
posisi,
|
||||
kelompok_peminatan,
|
||||
lokasi,
|
||||
jobdesk,
|
||||
kualifikasi,
|
||||
benefit,
|
||||
durasi_awal,
|
||||
durasi_akhir,
|
||||
paid
|
||||
} = req.body;
|
||||
|
||||
if (!posisi || !kelompok_peminatan || !lokasi || !jobdesk || !durasi_awal || !durasi_akhir || !paid) {
|
||||
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const batch = await getOrCreateActiveBatch();
|
||||
|
||||
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
|
||||
|
||||
const id = `lowongan-${Date.now()}`;
|
||||
|
||||
const lowongan = await prisma.lowonganMagang.create({
|
||||
data: {
|
||||
id,
|
||||
idBatch: batch.id,
|
||||
posisi,
|
||||
kelompokPeminatan: kelompok_peminatan,
|
||||
imagePath,
|
||||
jobdesk,
|
||||
lokasi,
|
||||
kualifikasi: kualifikasi || '',
|
||||
benefit: benefit || '',
|
||||
durasiAwal: new Date(durasi_awal),
|
||||
durasiAkhir: new Date(durasi_akhir),
|
||||
statusLowongan: 'DIBUKA',
|
||||
paid: paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID'
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: 'Internship lowongan berhasil ditambahkan',
|
||||
data: mapToFrontend(lowongan)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PATCH /api/lowongan-magang-api/update/:id
|
||||
* @desc Update data lowongan magang (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
posisi,
|
||||
kelompok_peminatan,
|
||||
lokasi,
|
||||
jobdesk,
|
||||
kualifikasi,
|
||||
benefit,
|
||||
durasi_awal,
|
||||
durasi_akhir,
|
||||
paid,
|
||||
status_lowongan
|
||||
} = req.body;
|
||||
|
||||
const existing = await prisma.lowonganMagang.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Lowongan tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
let imagePath = existing.imagePath;
|
||||
if (req.file) {
|
||||
imagePath = `/uploads/images/${req.file.filename}`;
|
||||
}
|
||||
|
||||
const updated = await prisma.lowonganMagang.update({
|
||||
where: { id },
|
||||
data: {
|
||||
posisi: posisi ?? existing.posisi,
|
||||
kelompokPeminatan: kelompok_peminatan ?? existing.kelompokPeminatan,
|
||||
lokasi: lokasi ?? existing.lokasi,
|
||||
jobdesk: jobdesk ?? existing.jobdesk,
|
||||
kualifikasi: kualifikasi ?? existing.kualifikasi,
|
||||
benefit: benefit ?? existing.benefit,
|
||||
durasiAwal: durasi_awal ? new Date(durasi_awal) : existing.durasiAwal,
|
||||
durasiAkhir: durasi_akhir ? new Date(durasi_akhir) : existing.durasiAkhir,
|
||||
statusLowongan: (status_lowongan ?? existing.statusLowongan) === 'DIBUKA' ? 'DIBUKA' : 'DITUTUP',
|
||||
paid: paid ? (paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID') : existing.paid,
|
||||
imagePath
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Lowongan berhasil diupdate',
|
||||
data: mapToFrontend(updated)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/lowongan-magang-api/delete/:id
|
||||
* @desc Hapus lowongan magang (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const existing = await prisma.lowonganMagang.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Lowongan tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
await prisma.lowonganMagang.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Lowongan berhasil dihapus'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||
import prisma from '../lib/prisma';
|
||||
import { upload } from '../middleware/upload';
|
||||
import { CustomError } from '../middleware/errorHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Map data partnership dari DB ke format frontend
|
||||
function mapToFrontend(partner: any) {
|
||||
return {
|
||||
id: partner.id,
|
||||
nama_partner: partner.namaPartner,
|
||||
image_path: partner.imagePath,
|
||||
createdAt: partner.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @route GET /api/partnership-api/get
|
||||
* @desc Dapetin semua data partnerships
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const partners = await prisma.partnership.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: partners.map(mapToFrontend)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/partnership-api/get/:id
|
||||
* @desc Dapetin detail satu partnership
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const partner = await prisma.partnership.findFirst({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!partner) {
|
||||
const error: CustomError = new Error('Partnership tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: [mapToFrontend(partner)] // Kadang list/detail di frontend minta dibungkus array
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/partnership-api/add
|
||||
* @desc Tambah partnership baru (khusus admin ya)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { nama_partner } = req.body;
|
||||
if (!nama_partner) {
|
||||
const error: CustomError = new Error('Nama partnership harus diisi');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
const error: CustomError = new Error('Thumbnail/image harus diunggah');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const imagePath = `/uploads/images/${req.file.filename}`;
|
||||
|
||||
const partner = await prisma.partnership.create({
|
||||
data: {
|
||||
namaPartner: nama_partner,
|
||||
imagePath
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: 'Partnership berhasil ditambahkan',
|
||||
data: mapToFrontend(partner)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PATCH /api/partnership-api/update/:id
|
||||
* @desc Update data partnership (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { nama_partner } = req.body;
|
||||
|
||||
const existing = await prisma.partnership.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Partnership tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
let imagePath = existing.imagePath;
|
||||
if (req.file) {
|
||||
imagePath = `/uploads/images/${req.file.filename}`;
|
||||
}
|
||||
|
||||
const updated = await prisma.partnership.update({
|
||||
where: { id: parseInt(id, 10) },
|
||||
data: {
|
||||
namaPartner: nama_partner ?? existing.namaPartner,
|
||||
imagePath
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Partnership berhasil diupdate',
|
||||
data: mapToFrontend(updated)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/partnership-api/delete/:id
|
||||
* @desc Hapus data partnership (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const existing = await prisma.partnership.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Partnership tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
await prisma.partnership.delete({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Partnership berhasil dihapus'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,179 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||
import prisma from '../lib/prisma';
|
||||
import { upload } from '../middleware/upload';
|
||||
import { CustomError } from '../middleware/errorHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Map data research product dari DB ke format frontend
|
||||
function mapToFrontend(product: any) {
|
||||
return {
|
||||
id: product.id,
|
||||
nama_project: product.namaProject,
|
||||
deskripsi: product.deskripsi,
|
||||
link_project: product.linkProject,
|
||||
image_path: product.imagePath,
|
||||
createdAt: product.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @route GET /api/hasil-research-api/get
|
||||
* @desc Dapetin semua hasil riset/products
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const products = await prisma.hasilResearch.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: products.map(mapToFrontend)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/hasil-research-api/get/:id
|
||||
* @desc Dapetin detail satu hasil riset/product
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const product = await prisma.hasilResearch.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
const error: CustomError = new Error('Project tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
data: mapToFrontend(product)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/hasil-research-api/add
|
||||
* @desc Tambah hasil riset baru (khusus admin ya)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { nama_project, deskripsi, link_project } = req.body;
|
||||
|
||||
if (!nama_project || !deskripsi || !link_project) {
|
||||
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
|
||||
error.statusCode = 400;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
|
||||
|
||||
const product = await prisma.hasilResearch.create({
|
||||
data: {
|
||||
namaProject: nama_project,
|
||||
deskripsi,
|
||||
linkProject: link_project,
|
||||
imagePath
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: 'Research project berhasil ditambahkan',
|
||||
data: mapToFrontend(product)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PATCH /api/hasil-research-api/update/:id
|
||||
* @desc Update data hasil riset (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { nama_project, deskripsi, link_project } = req.body;
|
||||
|
||||
const existing = await prisma.hasilResearch.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Project tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
let imagePath = existing.imagePath;
|
||||
if (req.file) {
|
||||
imagePath = `/uploads/images/${req.file.filename}`;
|
||||
}
|
||||
|
||||
const updated = await prisma.hasilResearch.update({
|
||||
where: { id: parseInt(id, 10) },
|
||||
data: {
|
||||
namaProject: nama_project ?? existing.namaProject,
|
||||
deskripsi: deskripsi ?? existing.deskripsi,
|
||||
linkProject: link_project ?? existing.linkProject,
|
||||
imagePath
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Research project berhasil diupdate',
|
||||
data: mapToFrontend(updated)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/hasil-research-api/delete/:id
|
||||
* @desc Hapus data hasil riset (khusus admin)
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const existing = await prisma.hasilResearch.findUnique({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const error: CustomError = new Error('Project tidak ditemukan');
|
||||
error.statusCode = 404;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
await prisma.hasilResearch.delete({
|
||||
where: { id: parseInt(id, 10) }
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
status: true,
|
||||
message: 'Research project berhasil dihapus'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user