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 } ); } }