import { NextRequest, NextResponse } from 'next/server'; import * as XLSX from 'xlsx'; import supabase from '@/lib/db'; // GET method for testing the route export async function GET() { return NextResponse.json({ message: 'Upload route is working' }); } export async function POST(request: NextRequest) { try { // Get form data from request const formData = await request.formData(); const file = formData.get('file') as File; if (!file) { return NextResponse.json({ message: 'File tidak ditemukan' }, { status: 400 }); } // Validate file size (max 10MB) if (file.size > 10 * 1024 * 1024) { return NextResponse.json({ message: 'File terlalu besar. Maksimal 10MB' }, { status: 400 }); } // Process file data based on file type let validData = []; let errors: string[] = []; if (file.name.endsWith('.csv') || file.type === 'text/csv') { // Process as CSV const fileContent = await file.text(); const result = await processCSVData(fileContent); validData = result.validData; errors = result.errors; } else { // Process as Excel const fileBuffer = await file.arrayBuffer(); const result = await processExcelData(fileBuffer); validData = result.validData; errors = result.errors; } if (validData.length === 0) { return NextResponse.json({ message: 'Tidak ada data valid yang ditemukan dalam file', errors }, { status: 400 }); } // Insert valid data into the database const { imported, errorCount, errorMessages } = await insertDataToDatabase(validData); // Combine all error messages const allErrors = [...errors, ...errorMessages]; return NextResponse.json({ message: 'Upload berhasil', imported, errors: errorCount, errorDetails: allErrors.length > 0 ? allErrors : undefined }); } catch (error) { console.error('Error uploading file:', error); return NextResponse.json( { message: `Terjadi kesalahan: ${(error as Error).message}` }, { status: 500 } ); } } // Function to process Excel data async function processExcelData(fileBuffer: ArrayBuffer) { try { // Parse Excel file const workbook = XLSX.read(fileBuffer, { type: 'array' }); if (!workbook.SheetNames || workbook.SheetNames.length === 0) { return { validData: [], errors: ['File Excel tidak memiliki sheet'] }; } // Get first sheet const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; if (!worksheet) { return { validData: [], errors: ['Sheet pertama tidak ditemukan'] }; } // Convert to JSON with proper typing const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]; if (jsonData.length === 0) { return { validData: [], errors: ['File Excel kosong'] }; } if (jsonData.length < 2) { return { validData: [], errors: ['File Excel hanya memiliki header, tidak ada data'] }; } // Convert Excel data to CSV-like format for processing const headers = jsonData[0].map(h => String(h).toLowerCase()); const rows = jsonData.slice(1); // Process the data using the common function return processData(headers, rows); } catch (error) { console.error('Error processing Excel data:', error); return { validData: [], errors: [`Error memproses file Excel: ${(error as Error).message}`] }; } } // Function to process CSV data async function processCSVData(fileContent: string) { const lines = fileContent.split(/\r?\n/).filter(line => line.trim() !== ''); if (lines.length === 0) { return { validData: [], errors: ['File CSV kosong'] }; } // Get headers from first line const headerLine = lines[0].toLowerCase(); const headers = headerLine.split(',').map(h => h.trim()); // Process data rows const rows = lines.slice(1).map(line => line.split(',').map(v => v.trim())); return processData(headers, rows); } // Common function to process data regardless of source format function processData(headers: string[], rows: any[][]) { // Define expected headers and their possible variations const expectedHeaderMap = { nim: ['nim', 'nomor induk', 'nomor mahasiswa'], nama_beasiswa: ['nama_beasiswa', 'nama beasiswa', 'namabeasiswa', 'beasiswa', 'nama'], sumber_beasiswa: ['sumber_beasiswa', 'sumber beasiswa', 'sumberbeasiswa', 'sumber'], jenis_beasiswa: ['jenis_beasiswa', 'jenis beasiswa', 'jenisbeasiswa', 'jenis'] }; // Map actual headers to expected headers const headerMap: { [key: string]: number } = {}; for (const [expectedHeader, variations] of Object.entries(expectedHeaderMap)) { const index = headers.findIndex(h => variations.some(variation => h.includes(variation)) ); if (index !== -1) { headerMap[expectedHeader] = index; } } // Check required headers const requiredHeaders = ['nim', 'nama_beasiswa', 'sumber_beasiswa', 'jenis_beasiswa']; const missingHeaders = requiredHeaders.filter(h => headerMap[h] === undefined); if (missingHeaders.length > 0) { return { validData: [], errors: [`Kolom berikut tidak ditemukan: ${missingHeaders.join(', ')}. Pastikan file memiliki kolom: NIM, Nama Beasiswa, Sumber Beasiswa, dan Jenis Beasiswa.`] }; } const validData = []; const errors = []; const validJenis = ['Pemerintah', 'Non-Pemerintah']; // Process data rows for (let i = 0; i < rows.length; i++) { const values = rows[i]; if (!values || values.length === 0) continue; try { // Extract values using header map const nim = String(values[headerMap.nim] || '').trim(); const nama_beasiswa = String(values[headerMap.nama_beasiswa] || '').trim(); const sumber_beasiswa = String(values[headerMap.sumber_beasiswa] || '').trim(); let jenis_beasiswa = String(values[headerMap.jenis_beasiswa] || '').trim(); // Validate required fields if (!nim || !nama_beasiswa || !sumber_beasiswa || !jenis_beasiswa) { errors.push(`Baris ${i+2}: Data tidak lengkap (NIM: ${nim || 'kosong'})`); continue; } // Normalize jenis beasiswa jenis_beasiswa = normalizeJenisBeasiswa(jenis_beasiswa); // Validate jenis beasiswa if (!validJenis.includes(jenis_beasiswa)) { errors.push(`Baris ${i+2}: Jenis beasiswa tidak valid "${jenis_beasiswa}" untuk NIM ${nim}. Harus salah satu dari: ${validJenis.join(', ')}`); continue; } // Add to valid data validData.push({ nim, nama_beasiswa, sumber_beasiswa, jenis_beasiswa }); } catch (error) { errors.push(`Baris ${i+2}: Error memproses data - ${(error as Error).message}`); } } return { validData, errors }; } // Function to normalize jenis beasiswa values function normalizeJenisBeasiswa(value: string): string { const lowerValue = value.toLowerCase(); if (['pemerintah', 'government', 'p', 'gov'].includes(lowerValue)) { return 'Pemerintah'; } if (['non-pemerintah', 'non pemerintah', 'nonpemerintah', 'swasta', 'private', 'np', 'non'].includes(lowerValue)) { return 'Non-Pemerintah'; } return value; // Return original if no match } // Function to insert data into database async function insertDataToDatabase(data: any[]) { let imported = 0; let errorCount = 0; const errorMessages: string[] = []; // First, validate all NIMs exist before processing const uniqueNims = [...new Set(data.map(item => item.nim))]; const nimValidationMap = new Map(); // Batch check all NIMs for existence for (const nim of uniqueNims) { try { const { data: mahasiswaData, error: checkError } = await supabase .from('mahasiswa') .select('id_mahasiswa, nama') .eq('nim', nim) .single(); if (checkError || !mahasiswaData) { nimValidationMap.set(nim, { exists: false, error: 'Mahasiswa dengan NIM ini tidak ditemukan dalam database' }); } else { nimValidationMap.set(nim, { exists: true, id_mahasiswa: mahasiswaData.id_mahasiswa, nama: mahasiswaData.nama }); } } catch (error) { nimValidationMap.set(nim, { exists: false, error: `Error checking NIM: ${(error as Error).message}` }); } } // Process each data item for (const item of data) { try { const nimValidation = nimValidationMap.get(item.nim); if (!nimValidation || !nimValidation.exists) { errorCount++; const errorMsg = nimValidation?.error || `NIM ${item.nim}: Mahasiswa dengan NIM ini tidak ditemukan dalam database`; errorMessages.push(errorMsg); continue; } // Check if beasiswa already exists for this mahasiswa and nama_beasiswa const { data: existingBeasiswa, error: beasiswaCheckError } = await supabase .from('beasiswa_mahasiswa') .select('id_beasiswa') .eq('id_mahasiswa', nimValidation.id_mahasiswa) .eq('nama_beasiswa', item.nama_beasiswa) .single(); if (existingBeasiswa) { // Update existing beasiswa const { error: updateError } = await supabase .from('beasiswa_mahasiswa') .update({ sumber_beasiswa: item.sumber_beasiswa, jenis_beasiswa: item.jenis_beasiswa }) .eq('id_beasiswa', existingBeasiswa.id_beasiswa); if (updateError) { errorCount++; const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal memperbarui beasiswa: ${updateError.message}`; errorMessages.push(errorMsg); continue; } } else { // Insert new beasiswa const { error: insertError } = await supabase .from('beasiswa_mahasiswa') .insert({ id_mahasiswa: nimValidation.id_mahasiswa, nama_beasiswa: item.nama_beasiswa, sumber_beasiswa: item.sumber_beasiswa, jenis_beasiswa: item.jenis_beasiswa }); if (insertError) { errorCount++; const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal menyimpan beasiswa: ${insertError.message}`; errorMessages.push(errorMsg); continue; } } imported++; } catch (error) { errorCount++; errorMessages.push(`NIM ${item.nim}: Terjadi kesalahan: ${(error as Error).message}`); } } return { imported, errorCount, errorMessages }; }