again n again

This commit is contained in:
Randa Firman Putra
2025-11-06 18:31:44 +07:00
parent 133ec36510
commit c77321bc8a
36 changed files with 6363 additions and 185 deletions

View File

@@ -0,0 +1,393 @@
import { NextRequest, NextResponse } from 'next/server';
import supabase from '@/lib/db';
// Define the MataKuliah type
interface MataKuliah {
id_mk: number;
kode_mk: string;
nama_mk: string;
sks: number;
semester: number;
jenis_mk: 'Wajib' | 'Pilihan Wajib' | 'Pilihan';
id_prasyarat: number | null;
nama_prasyarat?: string | null;
created_at: string;
updated_at: string;
}
// GET - Ambil semua data mata kuliah atau satu mata kuliah spesifik berdasarkan ID
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
const search = searchParams.get('search');
const semester = searchParams.get('semester');
if (id) {
// Ambil mata kuliah spesifik berdasarkan ID dengan join untuk prasyarat
const { data, error } = await supabase
.from('mata_kuliah')
.select(`
id_mk,
kode_mk,
nama_mk,
sks,
semester,
jenis_mk,
id_prasyarat,
created_at,
updated_at,
prasyarat:mata_kuliah!id_prasyarat(kode_mk, nama_mk)
`)
.eq('id_mk', id)
.single();
if (error || !data) {
return NextResponse.json({ message: 'Mata kuliah not found' }, { status: 404 });
}
// Transformasi data untuk meratakan field yang di-join
const transformedData = {
...data,
nama_prasyarat: (data.prasyarat as any)?.nama_mk || null
};
delete (transformedData as any).prasyarat;
return NextResponse.json(transformedData);
} else {
// Ambil semua mata kuliah terlebih dahulu
let query = supabase
.from('mata_kuliah')
.select('*');
// Add search condition if provided
if (search) {
query = query.or(`kode_mk.ilike.%${search}%,nama_mk.ilike.%${search}%`);
}
// Add semester filter if provided
if (semester && semester !== 'all') {
query = query.eq('semester', parseInt(semester));
}
// Add order by
query = query.order('semester', { ascending: true }).order('kode_mk', { ascending: true });
const { data: mataKuliahData, error } = await query;
if (error) {
console.error('Error fetching data:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
// Ambil data prasyarat untuk semua mata kuliah yang memiliki prasyarat
const prasyaratIds = mataKuliahData
.filter(mk => mk.id_prasyarat)
.map(mk => mk.id_prasyarat);
let prasyaratData: any[] = [];
if (prasyaratIds.length > 0) {
const { data: prasyaratResult, error: prasyaratError } = await supabase
.from('mata_kuliah')
.select('id_mk, kode_mk, nama_mk')
.in('id_mk', prasyaratIds);
if (!prasyaratError) {
prasyaratData = prasyaratResult || [];
}
}
// Gabungkan data mata kuliah dengan data prasyarat
const transformedData = mataKuliahData.map(item => {
const prasyarat = prasyaratData.find(p => p.id_mk === item.id_prasyarat);
return {
...item,
nama_prasyarat: prasyarat ? prasyarat.nama_mk : null
};
});
return NextResponse.json(transformedData);
}
} catch (error) {
console.error('Error fetching data:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}
// POST - Buat data mata kuliah baru
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
kode_mk,
nama_mk,
sks,
semester,
jenis_mk,
id_prasyarat
} = body;
// Validasi field yang wajib diisi
if (!kode_mk || !nama_mk || !sks || !semester || !jenis_mk) {
return NextResponse.json(
{ message: 'Missing required fields: kode_mk, nama_mk, sks, semester, jenis_mk' },
{ status: 400 }
);
}
// Validasi nilai sks dan semester
if (sks <= 0 || semester <= 0) {
return NextResponse.json(
{ message: 'SKS and semester must be greater than 0' },
{ status: 400 }
);
}
// Validasi jenis mata kuliah
const validJenisMK = ['Wajib', 'Pilihan Wajib', 'Pilihan'];
if (!validJenisMK.includes(jenis_mk)) {
return NextResponse.json(
{ message: 'Invalid jenis_mk value. Must be one of: Wajib, Pilihan Wajib, Pilihan' },
{ status: 400 }
);
}
// Cek apakah kode mata kuliah sudah ada
const { data: existing, error: checkError } = await supabase
.from('mata_kuliah')
.select('kode_mk')
.eq('kode_mk', kode_mk)
.single();
if (existing) {
return NextResponse.json(
{ message: 'Mata kuliah with this code already exists' },
{ status: 409 }
);
}
// Validasi prasyarat jika ada
if (id_prasyarat) {
const { data: prasyaratExists, error: prasyaratError } = await supabase
.from('mata_kuliah')
.select('id_mk')
.eq('id_mk', id_prasyarat)
.single();
if (prasyaratError || !prasyaratExists) {
return NextResponse.json(
{ message: 'Prasyarat mata kuliah not found' },
{ status: 404 }
);
}
}
// Insert mata kuliah baru
const { data, error } = await supabase
.from('mata_kuliah')
.insert({
kode_mk,
nama_mk,
sks: parseInt(sks),
semester: parseInt(semester),
jenis_mk,
id_prasyarat: id_prasyarat || null
})
.select()
.single();
if (error) {
console.error('Error creating mata kuliah:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
return NextResponse.json(
{ message: 'Mata kuliah created successfully', id: data.id_mk },
{ status: 201 }
);
} catch (error) {
console.error('Error creating mata kuliah:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}
// PUT - Update data mata kuliah yang sudah ada
export async function PUT(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
}
const body = await request.json();
const {
kode_mk,
nama_mk,
sks,
semester,
jenis_mk,
id_prasyarat
} = body;
// Validasi field yang wajib diisi
if (!kode_mk || !nama_mk || !sks || !semester || !jenis_mk) {
return NextResponse.json(
{ message: 'Missing required fields: kode_mk, nama_mk, sks, semester, jenis_mk' },
{ status: 400 }
);
}
// Validasi nilai sks dan semester
if (sks <= 0 || semester <= 0) {
return NextResponse.json(
{ message: 'SKS and semester must be greater than 0' },
{ status: 400 }
);
}
// Validasi jenis mata kuliah
const validJenisMK = ['Wajib', 'Pilihan Wajib', 'Pilihan'];
if (!validJenisMK.includes(jenis_mk)) {
return NextResponse.json(
{ message: 'Invalid jenis_mk value. Must be one of: Wajib, Pilihan Wajib, Pilihan' },
{ status: 400 }
);
}
// Cek apakah mata kuliah ada
const { data: existing, error: checkError } = await supabase
.from('mata_kuliah')
.select('*')
.eq('id_mk', id)
.single();
if (checkError || !existing) {
return NextResponse.json({ message: 'Mata kuliah not found' }, { status: 404 });
}
// Cek apakah kode mata kuliah sudah digunakan oleh mata kuliah lain
const { data: duplicateCode, error: duplicateError } = await supabase
.from('mata_kuliah')
.select('id_mk')
.eq('kode_mk', kode_mk)
.neq('id_mk', id)
.single();
if (duplicateCode) {
return NextResponse.json(
{ message: 'Mata kuliah with this code already exists' },
{ status: 409 }
);
}
// Validasi prasyarat jika ada
if (id_prasyarat) {
// Pastikan prasyarat tidak sama dengan mata kuliah itu sendiri
if (parseInt(id_prasyarat) === parseInt(id)) {
return NextResponse.json(
{ message: 'Mata kuliah cannot be prerequisite of itself' },
{ status: 400 }
);
}
const { data: prasyaratExists, error: prasyaratError } = await supabase
.from('mata_kuliah')
.select('id_mk')
.eq('id_mk', id_prasyarat)
.single();
if (prasyaratError || !prasyaratExists) {
return NextResponse.json(
{ message: 'Prasyarat mata kuliah not found' },
{ status: 404 }
);
}
}
// Update mata kuliah
const { error } = await supabase
.from('mata_kuliah')
.update({
kode_mk,
nama_mk,
sks: parseInt(sks),
semester: parseInt(semester),
jenis_mk,
id_prasyarat: id_prasyarat || null,
updated_at: new Date().toISOString()
})
.eq('id_mk', id);
if (error) {
console.error('Error updating mata kuliah:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
return NextResponse.json({ message: 'Mata kuliah updated successfully' });
} catch (error) {
console.error('Error updating mata kuliah:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}
// DELETE - Hapus data mata kuliah
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
}
// Check if mata kuliah exists
const { data: existing, error: checkError } = await supabase
.from('mata_kuliah')
.select('id_mk, kode_mk, nama_mk')
.eq('id_mk', id)
.single();
if (checkError || !existing) {
return NextResponse.json({ message: 'Mata kuliah not found' }, { status: 404 });
}
// Check if mata kuliah is used as prerequisite by other mata kuliah
const { data: dependents, error: dependentsError } = await supabase
.from('mata_kuliah')
.select('kode_mk, nama_mk')
.eq('id_prasyarat', id);
if (dependentsError) {
console.error('Error checking dependents:', dependentsError);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
if (dependents && dependents.length > 0) {
const dependentNames = dependents.map(d => `${d.kode_mk} - ${d.nama_mk}`).join(', ');
return NextResponse.json(
{
message: `Cannot delete mata kuliah. It is used as prerequisite by: ${dependentNames}`
},
{ status: 400 }
);
}
// Hapus mata kuliah
const { error } = await supabase
.from('mata_kuliah')
.delete()
.eq('id_mk', id);
if (error) {
console.error('Error deleting mata kuliah:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
return NextResponse.json({ message: 'Mata kuliah deleted successfully' });
} catch (error) {
console.error('Error deleting mata kuliah:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,420 @@
import { NextRequest, NextResponse } from 'next/server';
import supabase from '@/lib/db';
// Define the NilaiMahasiswa type
interface NilaiMahasiswa {
id_nilai: number;
id_mahasiswa: number;
id_mk: number;
nim: string;
nama: string;
kode_mk: string;
nama_mk: string;
nilai_huruf: 'A' | 'B+' | 'B' | 'C+' | 'C' | 'D+' | 'D' | 'E';
nilai_angka: number;
semester: number;
created_at: string;
updated_at: string;
}
// GET - Ambil semua data nilai mahasiswa atau satu nilai spesifik berdasarkan ID
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
const search = searchParams.get('search');
const semester = searchParams.get('semester');
const nilai_huruf = searchParams.get('nilai_huruf');
if (id) {
// Ambil nilai spesifik berdasarkan ID dengan join ke mahasiswa dan mata_kuliah
const { data, error } = await supabase
.from('nilai_mahasiswa')
.select(`
id_nilai,
id_mahasiswa,
id_mk,
nilai_huruf,
nilai_angka,
semester,
created_at,
updated_at,
mahasiswa!inner(nim, nama),
mata_kuliah!inner(kode_mk, nama_mk)
`)
.eq('id_nilai', id)
.single();
if (error || !data) {
return NextResponse.json({ message: 'Nilai mahasiswa not found' }, { status: 404 });
}
// Transformasi data untuk meratakan field yang di-join
const transformedData: any = {
...data,
nim: (data.mahasiswa as any)?.nim || '',
nama: (data.mahasiswa as any)?.nama || '',
kode_mk: (data.mata_kuliah as any)?.kode_mk || '',
nama_mk: (data.mata_kuliah as any)?.nama_mk || ''
};
delete transformedData.mahasiswa;
delete transformedData.mata_kuliah;
return NextResponse.json(transformedData);
} else {
// Ambil semua nilai mahasiswa dengan join
let query = supabase
.from('nilai_mahasiswa')
.select(`
id_nilai,
id_mahasiswa,
id_mk,
nilai_huruf,
nilai_angka,
semester,
created_at,
updated_at,
mahasiswa!inner(nim, nama),
mata_kuliah!inner(kode_mk, nama_mk)
`);
// Add search condition if provided
if (search) {
query = query.or(`mahasiswa.nim.ilike.%${search}%,mahasiswa.nama.ilike.%${search}%,mata_kuliah.kode_mk.ilike.%${search}%,mata_kuliah.nama_mk.ilike.%${search}%`);
}
// Add semester filter if provided
if (semester && semester !== 'all') {
query = query.eq('semester', parseInt(semester));
}
// Add nilai_huruf filter if provided
if (nilai_huruf && nilai_huruf !== 'all') {
query = query.eq('nilai_huruf', nilai_huruf);
}
// Add order by
query = query.order('semester', { ascending: true }).order('mahasiswa(nim)', { ascending: true });
const { data, error } = await query;
if (error) {
console.error('Error fetching data:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
// Transformasi data untuk meratakan field yang di-join
const transformedData = data.map((item: any) => ({
...item,
nim: item.mahasiswa?.nim || '',
nama: item.mahasiswa?.nama || '',
kode_mk: item.mata_kuliah?.kode_mk || '',
nama_mk: item.mata_kuliah?.nama_mk || ''
})).map((item: any) => {
const { mahasiswa, mata_kuliah, ...rest } = item;
return rest;
});
return NextResponse.json(transformedData);
}
} catch (error) {
console.error('Error fetching data:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}
// POST - Buat data nilai mahasiswa baru
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
nim,
id_mk,
nilai_huruf,
nilai_angka,
semester
} = body;
// Validasi field yang wajib diisi
if (!nim || !id_mk || !nilai_huruf || nilai_angka === undefined || !semester) {
return NextResponse.json(
{ message: 'Missing required fields: nim, id_mk, nilai_huruf, nilai_angka, semester' },
{ status: 400 }
);
}
// Validasi nilai huruf
const validNilaiHuruf = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'E'];
if (!validNilaiHuruf.includes(nilai_huruf)) {
return NextResponse.json(
{ message: 'Invalid nilai_huruf. Must be one of: A, B+, B, C+, C, D+, D, E' },
{ status: 400 }
);
}
// Validasi nilai angka
if (nilai_angka < 0 || nilai_angka > 4) {
return NextResponse.json(
{ message: 'nilai_angka must be between 0 and 4' },
{ status: 400 }
);
}
// Validasi semester
if (semester <= 0) {
return NextResponse.json(
{ message: 'semester must be greater than 0' },
{ status: 400 }
);
}
// Cek apakah mahasiswa dengan NIM tersebut ada
const { data: mahasiswa, error: mahasiswaError } = await supabase
.from('mahasiswa')
.select('id_mahasiswa, nim, nama')
.eq('nim', nim)
.single();
if (mahasiswaError || !mahasiswa) {
return NextResponse.json(
{ message: `Mahasiswa dengan NIM ${nim} tidak terdaftar` },
{ status: 404 }
);
}
// Cek apakah mata kuliah ada
const { data: mataKuliah, error: mkError } = await supabase
.from('mata_kuliah')
.select('id_mk, kode_mk, nama_mk')
.eq('id_mk', id_mk)
.single();
if (mkError || !mataKuliah) {
return NextResponse.json(
{ message: 'Mata kuliah not found' },
{ status: 404 }
);
}
// Cek apakah nilai untuk mahasiswa dan mata kuliah ini sudah ada
const { data: existingNilai, error: checkError } = await supabase
.from('nilai_mahasiswa')
.select('id_nilai')
.eq('id_mahasiswa', mahasiswa.id_mahasiswa)
.eq('id_mk', id_mk)
.single();
if (existingNilai) {
return NextResponse.json(
{ message: `Nilai untuk mahasiswa ${nim} pada mata kuliah ${mataKuliah.kode_mk} sudah ada` },
{ status: 409 }
);
}
// Insert nilai baru
const { data, error } = await supabase
.from('nilai_mahasiswa')
.insert({
id_mahasiswa: mahasiswa.id_mahasiswa,
id_mk: parseInt(id_mk),
nilai_huruf,
nilai_angka: parseFloat(nilai_angka),
semester: parseInt(semester)
})
.select()
.single();
if (error) {
console.error('Error creating nilai mahasiswa:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
return NextResponse.json(
{
message: 'Nilai mahasiswa berhasil ditambahkan',
id: data.id_nilai,
mahasiswa: mahasiswa.nama,
mata_kuliah: mataKuliah.nama_mk
},
{ status: 201 }
);
} catch (error) {
console.error('Error creating nilai mahasiswa:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}
// PUT - Update data nilai mahasiswa yang sudah ada
export async function PUT(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
}
const body = await request.json();
const {
nim,
id_mk,
nilai_huruf,
nilai_angka,
semester
} = body;
// Validasi field yang wajib diisi
if (!nim || !id_mk || !nilai_huruf || nilai_angka === undefined || !semester) {
return NextResponse.json(
{ message: 'Missing required fields: nim, id_mk, nilai_huruf, nilai_angka, semester' },
{ status: 400 }
);
}
// Validasi nilai huruf
const validNilaiHuruf = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'E'];
if (!validNilaiHuruf.includes(nilai_huruf)) {
return NextResponse.json(
{ message: 'Invalid nilai_huruf. Must be one of: A, B+, B, C+, C, D+, D, E' },
{ status: 400 }
);
}
// Validasi nilai angka
if (nilai_angka < 0 || nilai_angka > 4) {
return NextResponse.json(
{ message: 'nilai_angka must be between 0 and 4' },
{ status: 400 }
);
}
// Validasi semester
if (semester <= 0) {
return NextResponse.json(
{ message: 'semester must be greater than 0' },
{ status: 400 }
);
}
// Cek apakah nilai ada
const { data: existing, error: checkError } = await supabase
.from('nilai_mahasiswa')
.select('*')
.eq('id_nilai', id)
.single();
if (checkError || !existing) {
return NextResponse.json({ message: 'Nilai mahasiswa not found' }, { status: 404 });
}
// Cek apakah mahasiswa dengan NIM tersebut ada
const { data: mahasiswa, error: mahasiswaError } = await supabase
.from('mahasiswa')
.select('id_mahasiswa, nim, nama')
.eq('nim', nim)
.single();
if (mahasiswaError || !mahasiswa) {
return NextResponse.json(
{ message: `Mahasiswa dengan NIM ${nim} tidak terdaftar` },
{ status: 404 }
);
}
// Cek apakah mata kuliah ada
const { data: mataKuliah, error: mkError } = await supabase
.from('mata_kuliah')
.select('id_mk, kode_mk, nama_mk')
.eq('id_mk', id_mk)
.single();
if (mkError || !mataKuliah) {
return NextResponse.json(
{ message: 'Mata kuliah not found' },
{ status: 404 }
);
}
// Cek apakah kombinasi mahasiswa-mata kuliah sudah digunakan oleh nilai lain
const { data: duplicateNilai, error: duplicateError } = await supabase
.from('nilai_mahasiswa')
.select('id_nilai')
.eq('id_mahasiswa', mahasiswa.id_mahasiswa)
.eq('id_mk', id_mk)
.neq('id_nilai', id)
.single();
if (duplicateNilai) {
return NextResponse.json(
{ message: `Nilai untuk mahasiswa ${nim} pada mata kuliah ${mataKuliah.kode_mk} sudah ada` },
{ status: 409 }
);
}
// Update nilai
const { error } = await supabase
.from('nilai_mahasiswa')
.update({
id_mahasiswa: mahasiswa.id_mahasiswa,
id_mk: parseInt(id_mk),
nilai_huruf,
nilai_angka: parseFloat(nilai_angka),
semester: parseInt(semester),
updated_at: new Date().toISOString()
})
.eq('id_nilai', id);
if (error) {
console.error('Error updating nilai mahasiswa:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
return NextResponse.json({ message: 'Nilai mahasiswa berhasil diperbarui' });
} catch (error) {
console.error('Error updating nilai mahasiswa:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}
// DELETE - Hapus data nilai mahasiswa
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
}
// Check if nilai exists
const { data: existing, error: checkError } = await supabase
.from('nilai_mahasiswa')
.select(`
id_nilai,
mahasiswa!inner(nim, nama),
mata_kuliah!inner(kode_mk, nama_mk)
`)
.eq('id_nilai', id)
.single();
if (checkError || !existing) {
return NextResponse.json({ message: 'Nilai mahasiswa not found' }, { status: 404 });
}
// Hapus nilai
const { error } = await supabase
.from('nilai_mahasiswa')
.delete()
.eq('id_nilai', id);
if (error) {
console.error('Error deleting nilai mahasiswa:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
return NextResponse.json({ message: 'Nilai mahasiswa berhasil dihapus' });
} catch (error) {
console.error('Error deleting nilai mahasiswa:', error);
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,311 @@
import { NextRequest, NextResponse } from 'next/server';
import supabase from '@/lib/db';
import * as XLSX from 'xlsx';
interface NilaiMahasiswaUpload {
nim: string;
kode_mk: string;
semester: number;
nilai_huruf: 'A' | 'B+' | 'B' | 'C+' | 'C' | 'D+' | 'D' | 'E';
nilai_angka: number;
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ message: 'No file uploaded' },
{ status: 400 }
);
}
// Validate file type
const validTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'
];
if (!validTypes.includes(file.type)) {
return NextResponse.json(
{ message: 'Invalid file type. Please upload Excel (.xlsx, .xls) or CSV (.csv) file' },
{ status: 400 }
);
}
// Validate file size (10MB max)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
return NextResponse.json(
{ message: 'File size too large. Maximum size is 10MB' },
{ status: 400 }
);
}
// Read file content
const buffer = await file.arrayBuffer();
let data: any[] = [];
if (file.type === 'text/csv') {
// Handle CSV file
const text = new TextDecoder().decode(buffer);
const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) {
return NextResponse.json(
{ message: 'File must contain at least header and one data row' },
{ status: 400 }
);
}
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
const row: any = {};
headers.forEach((header, index) => {
row[header] = values[index] || '';
});
data.push(row);
}
} else {
// Handle Excel file
const workbook = XLSX.read(buffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
data = XLSX.utils.sheet_to_json(worksheet);
}
if (data.length === 0) {
return NextResponse.json(
{ message: 'No data found in file' },
{ status: 400 }
);
}
// Normalize column names and validate required columns
const normalizedData = data.map(row => {
const normalizedRow: any = {};
Object.keys(row).forEach(key => {
const normalizedKey = key.toLowerCase()
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_]/g, '');
normalizedRow[normalizedKey] = row[key];
});
return normalizedRow;
});
// Map common column variations to standard names
const columnMappings = {
'nim': ['nim', 'NIM', 'Nim'],
'kode_mk': ['kode_mk', 'Kode MK', 'kode mk', 'kodemk', 'kodeMK'],
'semester': ['semester', 'Semester', 'sem'],
'nilai_huruf': ['nilai_huruf', 'Nilai Huruf', 'nilai huruf', 'nilaihuruf', 'nilaiHuruf', 'grade'],
'nilai_angka': ['nilai_angka', 'Nilai Angka', 'nilai angka', 'nilaiangka', 'nilaiAngka', 'score']
};
// Further normalize data with column mappings
const finalData = normalizedData.map(row => {
const finalRow: any = {};
Object.keys(columnMappings).forEach(standardKey => {
const variations = columnMappings[standardKey as keyof typeof columnMappings];
let value = null;
// Try to find the value using different variations
for (const variation of variations) {
const normalizedVariation = variation.toLowerCase()
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_]/g, '');
if (row[normalizedVariation] !== undefined && row[normalizedVariation] !== null && row[normalizedVariation] !== '') {
value = row[normalizedVariation];
break;
}
}
finalRow[standardKey] = value;
});
return finalRow;
});
// Validate required columns
const requiredColumns = ['nim', 'kode_mk', 'semester', 'nilai_huruf', 'nilai_angka'];
const firstRow = finalData[0];
const missingColumns = requiredColumns.filter(col =>
firstRow[col] === undefined || firstRow[col] === null || firstRow[col] === ''
);
if (missingColumns.length > 0) {
return NextResponse.json(
{ message: `Missing required columns: ${missingColumns.join(', ')}. Please check your Excel headers.` },
{ status: 400 }
);
}
// Use the normalized data for processing
data = finalData;
// Process and validate data
const processedData: NilaiMahasiswaUpload[] = [];
const errors: string[] = [];
for (let i = 0; i < data.length; i++) {
const row = data[i];
const rowNum = i + 2; // +2 because Excel rows start from 1 and we skip header
try {
// Validate and convert data types
const semester = parseInt(row.semester);
// Handle decimal comma (Indonesian format) and convert to dot
const nilaiAngkaStr = row.nilai_angka.toString().replace(',', '.');
const nilai_angka = parseFloat(nilaiAngkaStr);
if (isNaN(semester) || semester <= 0 || semester > 8) {
errors.push(`Row ${rowNum}: Semester must be a number between 1-8`);
continue;
}
if (isNaN(nilai_angka) || nilai_angka < 0 || nilai_angka > 4) {
errors.push(`Row ${rowNum}: Nilai angka must be a number between 0-4 (current: ${row.nilai_angka})`);
continue;
}
// Validate nilai_huruf
const validNilaiHuruf = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'E'];
if (!validNilaiHuruf.includes(row.nilai_huruf)) {
errors.push(`Row ${rowNum}: nilai_huruf must be one of: ${validNilaiHuruf.join(', ')}`);
continue;
}
// Validate required fields
if (!row.nim || !row.kode_mk) {
errors.push(`Row ${rowNum}: nim and kode_mk are required`);
continue;
}
// Keep kode_mk as is - don't normalize it
let kodeMK = row.kode_mk.toString().trim();
processedData.push({
nim: row.nim.toString().trim(),
kode_mk: kodeMK,
semester,
nilai_huruf: row.nilai_huruf as 'A' | 'B+' | 'B' | 'C+' | 'C' | 'D+' | 'D' | 'E',
nilai_angka
});
} catch (error) {
errors.push(`Row ${rowNum}: Invalid data format`);
}
}
if (errors.length > 0) {
return NextResponse.json(
{
message: 'Validation errors found',
errors: errors.slice(0, 10) // Limit to first 10 errors
},
{ status: 400 }
);
}
// Get existing mahasiswa and mata kuliah for validation
const { data: existingMahasiswa, error: fetchMahasiswaError } = await supabase
.from('mahasiswa')
.select('id_mahasiswa, nim');
if (fetchMahasiswaError) {
return NextResponse.json(
{ message: 'Failed to validate mahasiswa data' },
{ status: 500 }
);
}
const { data: existingMataKuliah, error: fetchMKError } = await supabase
.from('mata_kuliah')
.select('id_mk, kode_mk');
if (fetchMKError) {
return NextResponse.json(
{ message: 'Failed to validate mata kuliah data' },
{ status: 500 }
);
}
const nimToIdMap = new Map(existingMahasiswa?.map(m => [m.nim, m.id_mahasiswa]) || []);
const kodeMKToIdMap = new Map(existingMataKuliah?.map(mk => [mk.kode_mk, mk.id_mk]) || []);
// Process data for insertion
const insertData: any[] = [];
const validationErrors: string[] = [];
for (const nilai of processedData) {
// Check if mahasiswa exists
const id_mahasiswa = nimToIdMap.get(nilai.nim);
if (!id_mahasiswa) {
validationErrors.push(`NIM ${nilai.nim} not found in database`);
continue;
}
// Check if mata kuliah exists
const id_mk = kodeMKToIdMap.get(nilai.kode_mk);
if (!id_mk) {
validationErrors.push(`Kode MK ${nilai.kode_mk} not found in database`);
continue;
}
insertData.push({
id_mahasiswa,
id_mk,
nilai_huruf: nilai.nilai_huruf,
nilai_angka: nilai.nilai_angka,
semester: nilai.semester
});
}
if (validationErrors.length > 0) {
return NextResponse.json(
{
message: 'Data validation errors',
errors: validationErrors.slice(0, 10)
},
{ status: 400 }
);
}
// Insert data to database
let successCount = 0;
if (insertData.length > 0) {
const { error: insertError } = await supabase
.from('nilai_mahasiswa')
.insert(insertData);
if (insertError) {
return NextResponse.json(
{ message: 'Failed to insert data to database' },
{ status: 500 }
);
}
successCount = insertData.length;
}
return NextResponse.json({
message: 'Upload completed',
successCount,
totalRows: processedData.length
});
} catch (error) {
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,257 @@
import { NextRequest, NextResponse } from 'next/server';
import supabase from '@/lib/db';
import * as XLSX from 'xlsx';
interface MataKuliahUpload {
kode_mk: string;
nama_mk: string;
sks: number;
semester: number;
jenis_mk: 'Wajib' | 'Pilihan Wajib' | 'Pilihan';
kode_prasyarat?: string;
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ message: 'No file uploaded' },
{ status: 400 }
);
}
// Validate file type
const validTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'
];
if (!validTypes.includes(file.type)) {
return NextResponse.json(
{ message: 'Invalid file type. Please upload Excel (.xlsx, .xls) or CSV (.csv) file' },
{ status: 400 }
);
}
// Validate file size (10MB max)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
return NextResponse.json(
{ message: 'File size too large. Maximum size is 10MB' },
{ status: 400 }
);
}
// Read file content
const buffer = await file.arrayBuffer();
let data: any[] = [];
if (file.type === 'text/csv') {
// Handle CSV file
const text = new TextDecoder().decode(buffer);
const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) {
return NextResponse.json(
{ message: 'File must contain at least header and one data row' },
{ status: 400 }
);
}
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
const row: any = {};
headers.forEach((header, index) => {
row[header] = values[index] || '';
});
data.push(row);
}
} else {
// Handle Excel file
const workbook = XLSX.read(buffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
data = XLSX.utils.sheet_to_json(worksheet);
}
if (data.length === 0) {
return NextResponse.json(
{ message: 'No data found in file' },
{ status: 400 }
);
}
// Validate required columns
const requiredColumns = ['kode_mk', 'nama_mk', 'sks', 'semester', 'jenis_mk'];
const firstRow = data[0];
const missingColumns = requiredColumns.filter(col => !(col in firstRow));
if (missingColumns.length > 0) {
return NextResponse.json(
{ message: `Missing required columns: ${missingColumns.join(', ')}` },
{ status: 400 }
);
}
// Process and validate data
const processedData: MataKuliahUpload[] = [];
const errors: string[] = [];
for (let i = 0; i < data.length; i++) {
const row = data[i];
const rowNum = i + 2; // +2 because Excel rows start from 1 and we skip header
try {
// Validate and convert data types
const sks = parseInt(row.sks);
const semester = parseInt(row.semester);
if (isNaN(sks) || sks <= 0 || sks > 6) {
errors.push(`Row ${rowNum}: SKS must be a number between 1-6`);
continue;
}
if (isNaN(semester) || semester <= 0 || semester > 8) {
errors.push(`Row ${rowNum}: Semester must be a number between 1-8`);
continue;
}
// Validate jenis_mk
const validJenisMK = ['Wajib', 'Pilihan Wajib', 'Pilihan'];
if (!validJenisMK.includes(row.jenis_mk)) {
errors.push(`Row ${rowNum}: jenis_mk must be one of: ${validJenisMK.join(', ')}`);
continue;
}
// Validate required fields
if (!row.kode_mk || !row.nama_mk) {
errors.push(`Row ${rowNum}: kode_mk and nama_mk are required`);
continue;
}
processedData.push({
kode_mk: row.kode_mk.toString().trim(),
nama_mk: row.nama_mk.toString().trim(),
sks,
semester,
jenis_mk: row.jenis_mk as 'Wajib' | 'Pilihan Wajib' | 'Pilihan',
kode_prasyarat: row.kode_prasyarat ? row.kode_prasyarat.toString().trim() : undefined
});
} catch (error) {
errors.push(`Row ${rowNum}: Invalid data format`);
}
}
if (errors.length > 0) {
return NextResponse.json(
{
message: 'Validation errors found',
errors: errors.slice(0, 10) // Limit to first 10 errors
},
{ status: 400 }
);
}
// Get existing mata kuliah for duplicate check and prasyarat validation
const { data: existingMK, error: fetchError } = await supabase
.from('mata_kuliah')
.select('kode_mk, id_mk');
if (fetchError) {
console.error('Error fetching existing mata kuliah:', fetchError);
return NextResponse.json(
{ message: 'Failed to validate data' },
{ status: 500 }
);
}
const existingCodes = new Set(existingMK?.map(mk => mk.kode_mk) || []);
const codeToIdMap = new Map(existingMK?.map(mk => [mk.kode_mk, mk.id_mk]) || []);
// Process data for insertion
const insertData: any[] = [];
const duplicates: string[] = [];
for (const mk of processedData) {
// Check for duplicates
if (existingCodes.has(mk.kode_mk)) {
duplicates.push(mk.kode_mk);
continue;
}
// Resolve prasyarat ID
let id_prasyarat = null;
if (mk.kode_prasyarat) {
id_prasyarat = codeToIdMap.get(mk.kode_prasyarat);
if (!id_prasyarat) {
errors.push(`Prasyarat ${mk.kode_prasyarat} not found for ${mk.kode_mk}`);
continue;
}
}
insertData.push({
kode_mk: mk.kode_mk,
nama_mk: mk.nama_mk,
sks: mk.sks,
semester: mk.semester,
jenis_mk: mk.jenis_mk,
id_prasyarat
});
}
if (errors.length > 0) {
return NextResponse.json(
{
message: 'Prasyarat validation errors',
errors: errors.slice(0, 10)
},
{ status: 400 }
);
}
// Insert data to database
let successCount = 0;
if (insertData.length > 0) {
const { error: insertError } = await supabase
.from('mata_kuliah')
.insert(insertData);
if (insertError) {
console.error('Error inserting mata kuliah:', insertError);
return NextResponse.json(
{ message: 'Failed to insert data to database' },
{ status: 500 }
);
}
successCount = insertData.length;
}
// Prepare response
const response: any = {
message: 'Upload completed',
successCount,
totalRows: processedData.length
};
if (duplicates.length > 0) {
response.duplicates = duplicates;
response.duplicateCount = duplicates.length;
}
return NextResponse.json(response);
} catch (error) {
console.error('Error processing upload:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}