again n again
This commit is contained in:
40
app/api/dosen/list/route.ts
Normal file
40
app/api/dosen/list/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
// GET - Ambil daftar nama dosen yang menjadi pembimbing
|
||||
export async function GET() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select(`
|
||||
dosen_pembimbing_1:dosen!pembimbing_1(nama_dosen),
|
||||
dosen_pembimbing_2:dosen!pembimbing_2(nama_dosen)
|
||||
`)
|
||||
.or('pembimbing_1.not.is.null,pembimbing_2.not.is.null');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching dosen list:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Extract unique dosen names
|
||||
const uniqueDosen = new Set<string>();
|
||||
|
||||
data.forEach((item: any) => {
|
||||
if (item.dosen_pembimbing_1?.nama_dosen) {
|
||||
uniqueDosen.add(item.dosen_pembimbing_1.nama_dosen);
|
||||
}
|
||||
if (item.dosen_pembimbing_2?.nama_dosen) {
|
||||
uniqueDosen.add(item.dosen_pembimbing_2.nama_dosen);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to sorted array
|
||||
const sortedDosen = Array.from(uniqueDosen).sort();
|
||||
|
||||
return NextResponse.json(sortedDosen);
|
||||
} catch (error) {
|
||||
console.error('Error fetching dosen list:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
393
app/api/keloladata/data-mata-kuliah/route.ts
Normal file
393
app/api/keloladata/data-mata-kuliah/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
420
app/api/keloladata/data-nilai-mahasiswa/route.ts
Normal file
420
app/api/keloladata/data-nilai-mahasiswa/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
311
app/api/keloladata/data-nilai-mahasiswa/upload/route.ts
Normal file
311
app/api/keloladata/data-nilai-mahasiswa/upload/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
257
app/api/keloladata/upload-mata-kuliah/route.ts
Normal file
257
app/api/keloladata/upload-mata-kuliah/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
65
app/api/tabeldetail/asal-daerah/route.ts
Normal file
65
app/api/tabeldetail/asal-daerah/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
interface MahasiswaAsalDaerah {
|
||||
nim: string;
|
||||
nama: string;
|
||||
tahun_angkatan: number;
|
||||
kabupaten: string;
|
||||
}
|
||||
|
||||
// GET - Ambil data mahasiswa dengan asal kabupaten dan filter tahun angkatan untuk tabel detail
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const tahunAngkatan = searchParams.get('tahun_angkatan');
|
||||
|
||||
let query = supabase
|
||||
.from('mahasiswa')
|
||||
.select(`
|
||||
nim,
|
||||
nama,
|
||||
tahun_angkatan,
|
||||
kabupaten
|
||||
`)
|
||||
.not('kabupaten', 'is', null) // Hanya ambil mahasiswa yang memiliki data kabupaten
|
||||
.order('nama', { ascending: true });
|
||||
|
||||
// Jika ada filter tahun angkatan, terapkan filter
|
||||
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||
query = query.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Failed to fetch data', error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Transform data untuk memastikan format yang konsisten
|
||||
const transformedData: MahasiswaAsalDaerah[] = (data || []).map((item: any) => ({
|
||||
nim: item.nim || '',
|
||||
nama: item.nama || '',
|
||||
tahun_angkatan: item.tahun_angkatan || 0,
|
||||
kabupaten: item.kabupaten || ''
|
||||
}));
|
||||
|
||||
return NextResponse.json(transformedData, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in tabeldetail/asal-daerah API:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
65
app/api/tabeldetail/asal-provinsi/route.ts
Normal file
65
app/api/tabeldetail/asal-provinsi/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
interface MahasiswaAsalProvinsi {
|
||||
nim: string;
|
||||
nama: string;
|
||||
tahun_angkatan: number;
|
||||
provinsi: string;
|
||||
}
|
||||
|
||||
// GET - Ambil data mahasiswa dengan asal provinsi dan filter tahun angkatan untuk tabel detail
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const tahunAngkatan = searchParams.get('tahun_angkatan');
|
||||
|
||||
let query = supabase
|
||||
.from('mahasiswa')
|
||||
.select(`
|
||||
nim,
|
||||
nama,
|
||||
tahun_angkatan,
|
||||
provinsi
|
||||
`)
|
||||
.not('provinsi', 'is', null) // Hanya ambil mahasiswa yang memiliki data provinsi
|
||||
.order('nama', { ascending: true });
|
||||
|
||||
// Jika ada filter tahun angkatan, terapkan filter
|
||||
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||
query = query.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Failed to fetch data', error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Transform data untuk memastikan format yang konsisten
|
||||
const transformedData: MahasiswaAsalProvinsi[] = (data || []).map((item: any) => ({
|
||||
nim: item.nim || '',
|
||||
nama: item.nama || '',
|
||||
tahun_angkatan: item.tahun_angkatan || 0,
|
||||
provinsi: item.provinsi || ''
|
||||
}));
|
||||
|
||||
return NextResponse.json(transformedData, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in tabeldetail/asal-provinsi API:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
82
app/api/tabeldetail/bimbingan-dosen/route.ts
Normal file
82
app/api/tabeldetail/bimbingan-dosen/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
interface MahasiswaBimbinganDosen {
|
||||
nim: string;
|
||||
nama: string;
|
||||
tahun_angkatan: number;
|
||||
nama_pembimbing_1: string | null;
|
||||
nama_pembimbing_2: string | null;
|
||||
status_bimbingan: string;
|
||||
}
|
||||
|
||||
// GET - Ambil data mahasiswa dengan pembimbing dan filter tahun angkatan serta nama dosen untuk tabel detail
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const tahunAngkatan = searchParams.get('tahun_angkatan');
|
||||
const namaDosen = searchParams.get('nama_dosen');
|
||||
|
||||
let query = supabase
|
||||
.from('mahasiswa')
|
||||
.select(`
|
||||
nim,
|
||||
nama,
|
||||
tahun_angkatan,
|
||||
status_bimbingan,
|
||||
pembimbing_1,
|
||||
pembimbing_2,
|
||||
dosen_pembimbing_1:dosen!pembimbing_1(nama_dosen),
|
||||
dosen_pembimbing_2:dosen!pembimbing_2(nama_dosen)
|
||||
`)
|
||||
.or('pembimbing_1.not.is.null,pembimbing_2.not.is.null') // Hanya ambil mahasiswa yang memiliki minimal satu pembimbing
|
||||
.order('nama', { ascending: true });
|
||||
|
||||
// Jika ada filter tahun angkatan, terapkan filter
|
||||
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||
query = query.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Failed to fetch data', error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Transform data untuk meratakan field yang di-join
|
||||
let transformedData: MahasiswaBimbinganDosen[] = (data || []).map((item: any) => ({
|
||||
nim: item.nim || '',
|
||||
nama: item.nama || '',
|
||||
tahun_angkatan: item.tahun_angkatan || 0,
|
||||
nama_pembimbing_1: item.dosen_pembimbing_1?.nama_dosen || null,
|
||||
nama_pembimbing_2: item.dosen_pembimbing_2?.nama_dosen || null,
|
||||
status_bimbingan: item.status_bimbingan || ''
|
||||
}));
|
||||
|
||||
// Filter berdasarkan nama dosen jika diberikan
|
||||
if (namaDosen && namaDosen !== 'all') {
|
||||
transformedData = transformedData.filter(mahasiswa =>
|
||||
mahasiswa.nama_pembimbing_1 === namaDosen ||
|
||||
mahasiswa.nama_pembimbing_2 === namaDosen
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(transformedData, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in tabeldetail/bimbingan-dosen API:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
69
app/api/tabeldetail/nama-beasiswa/route.ts
Normal file
69
app/api/tabeldetail/nama-beasiswa/route.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
interface MahasiswaNamaBeasiswa {
|
||||
nim: string;
|
||||
nama: string;
|
||||
tahun_angkatan: number;
|
||||
nama_beasiswa: string;
|
||||
}
|
||||
|
||||
// GET - Ambil data mahasiswa dengan nama beasiswa dan filter tahun angkatan untuk tabel detail
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const tahunAngkatan = searchParams.get('tahun_angkatan');
|
||||
|
||||
let query = supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.select(`
|
||||
nama_beasiswa,
|
||||
mahasiswa!inner(
|
||||
nim,
|
||||
nama,
|
||||
tahun_angkatan
|
||||
)
|
||||
`)
|
||||
.order('nama_beasiswa', { ascending: true });
|
||||
|
||||
// Jika ada filter tahun angkatan, terapkan filter
|
||||
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||
query = query.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Failed to fetch data', error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Transform data untuk meratakan field yang di-join
|
||||
const transformedData: MahasiswaNamaBeasiswa[] = (data || []).map((item: any) => ({
|
||||
nim: item.mahasiswa?.nim || '',
|
||||
nama: item.mahasiswa?.nama || '',
|
||||
tahun_angkatan: item.mahasiswa?.tahun_angkatan || 0,
|
||||
nama_beasiswa: item.nama_beasiswa || ''
|
||||
}));
|
||||
|
||||
// Urutkan berdasarkan nama mahasiswa
|
||||
transformedData.sort((a, b) => a.nama.localeCompare(b.nama));
|
||||
|
||||
return NextResponse.json(transformedData, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in tabeldetail/nama-beasiswa API:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
72
app/api/tabeldetail/tingkat-prestasi/route.ts
Normal file
72
app/api/tabeldetail/tingkat-prestasi/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
interface MahasiswaTingkatPrestasi {
|
||||
nim: string;
|
||||
nama: string;
|
||||
tahun_angkatan: number;
|
||||
tingkat_prestasi: string;
|
||||
nama_prestasi: string;
|
||||
}
|
||||
|
||||
// GET - Ambil data mahasiswa dengan tingkat prestasi dan filter tahun angkatan untuk tabel detail
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const tahunAngkatan = searchParams.get('tahun_angkatan');
|
||||
|
||||
let query = supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.select(`
|
||||
tingkat_prestasi,
|
||||
nama_prestasi,
|
||||
mahasiswa!inner(
|
||||
nim,
|
||||
nama,
|
||||
tahun_angkatan
|
||||
)
|
||||
`)
|
||||
.order('tingkat_prestasi', { ascending: true });
|
||||
|
||||
// Jika ada filter tahun angkatan, terapkan filter
|
||||
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||
query = query.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Failed to fetch data', error: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Transform data untuk meratakan field yang di-join
|
||||
const transformedData: MahasiswaTingkatPrestasi[] = (data || []).map((item: any) => ({
|
||||
nim: item.mahasiswa?.nim || '',
|
||||
nama: item.mahasiswa?.nama || '',
|
||||
tahun_angkatan: item.mahasiswa?.tahun_angkatan || 0,
|
||||
tingkat_prestasi: item.tingkat_prestasi || '',
|
||||
nama_prestasi: item.nama_prestasi || ''
|
||||
}));
|
||||
|
||||
// Urutkan berdasarkan nama mahasiswa
|
||||
transformedData.sort((a, b) => a.nama.localeCompare(b.nama));
|
||||
|
||||
return NextResponse.json(transformedData, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in tabeldetail/tingkat-prestasi API:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user