import { NextRequest, NextResponse } from 'next/server'; import * as XLSX from 'xlsx'; import supabase from '@/lib/db'; 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: 'File tidak ditemukan' }, { status: 400 }); } let validData = []; let errors: string[] = []; if (file.name.endsWith('.csv') || file.type === 'text/csv') { const fileContent = await file.text(); const result = await processCSVData(fileContent); validData = result.validData; errors = result.errors; } else { 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 }); } const { imported, errorCount, errorMessages } = await insertDataToDatabase(validData); 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 } ); } } async function processExcelData(fileBuffer: ArrayBuffer) { try { const workbook = XLSX.read(fileBuffer, { type: 'array', cellDates: true, dateNF: 'yyyy-mm-dd' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; let jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false, dateNF: 'yyyy-mm-dd' }) as any[][]; if (jsonData.length === 0) { return { validData: [], errors: ['File Excel kosong'] }; } jsonData = jsonData.map(row => { if (!row) return row; return row.map(cell => { if (cell && typeof cell === 'object' && 'toISOString' in cell) { return cell.toISOString().split('T')[0]; } return cell; }); }); const headers = jsonData[0].map(h => String(h).toLowerCase()); const rows = jsonData.slice(1); return processData(headers, rows); } catch (error) { return { validData: [], errors: [(error as Error).message] }; } } 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'] }; } const headerLine = lines[0].toLowerCase(); const headers = headerLine.split(',').map(h => h.trim()); const rows = lines.slice(1).map(line => line.split(',').map(v => v.trim())); return processData(headers, rows); } function processData(headers: string[], rows: any[][]) { const expectedHeaderMap = { nim: ['nim', 'nomor induk', 'nomor mahasiswa'], jenis_prestasi: ['jenis prestasi', 'jenis_prestasi', 'jenisprestasi'], nama_prestasi: ['nama prestasi', 'nama_prestasi', 'namaprestasi', 'prestasi'], tingkat_prestasi: ['tingkat prestasi', 'tingkat_prestasi', 'tingkatprestasi', 'tingkat'], peringkat: ['peringkat', 'ranking', 'juara', 'posisi'], tanggal_prestasi: ['tanggal prestasi', 'tanggal_prestasi', 'tanggalprestasi', 'tanggal'] }; const headerMap: { [key: string]: number } = {}; for (const [expectedHeader, variations] of Object.entries(expectedHeaderMap)) { const index = headers.findIndex(h => { if (!h) return false; const headerStr = String(h).toLowerCase().trim(); return variations.some(variation => headerStr === variation); }); if (index !== -1) { headerMap[expectedHeader] = index; } } for (const [expectedHeader, variations] of Object.entries(expectedHeaderMap)) { if (headerMap[expectedHeader] !== undefined) continue; const index = headers.findIndex(h => { if (!h) return false; const headerStr = String(h).toLowerCase().trim(); return variations.some(variation => headerStr.includes(variation)); }); if (index !== -1) { headerMap[expectedHeader] = index; } } const requiredHeaders = ['nim', 'jenis_prestasi', 'nama_prestasi', 'tingkat_prestasi', 'peringkat', 'tanggal_prestasi']; 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, Jenis Prestasi, Nama Prestasi, Tingkat Prestasi, Peringkat, dan Tanggal.`] }; } const validData = []; const errors = []; const validJenisPrestasi = ['Akademik', 'Non-Akademik']; const validTingkatPrestasi = ['Kabupaten', 'Provinsi', 'Nasional', 'Internasional']; for (let i = 0; i < rows.length; i++) { const values = rows[i]; if (!values || values.length === 0) continue; try { const nim = String(values[headerMap.nim] || '').trim(); let jenis_prestasi = String(values[headerMap.jenis_prestasi] || '').trim(); const nama_prestasi = String(values[headerMap.nama_prestasi] || '').trim(); let tingkat_prestasi = String(values[headerMap.tingkat_prestasi] || '').trim(); const peringkat = String(values[headerMap.peringkat] || '').trim(); let tanggal_prestasi = String(values[headerMap.tanggal_prestasi] || '').trim(); if (!nim || !jenis_prestasi || !nama_prestasi || !tingkat_prestasi || !peringkat || !tanggal_prestasi) { const errorMsg = `Baris ${i+2}: Data tidak lengkap (NIM: ${nim || 'kosong'})`; errors.push(errorMsg); continue; } jenis_prestasi = normalizeJenisPrestasi(jenis_prestasi); if (!validJenisPrestasi.includes(jenis_prestasi)) { const errorMsg = `Baris ${i+2}: Jenis prestasi tidak valid "${jenis_prestasi}" untuk NIM ${nim}. Harus salah satu dari: ${validJenisPrestasi.join(', ')}`; errors.push(errorMsg); continue; } tingkat_prestasi = normalizeTingkatPrestasi(tingkat_prestasi); if (!validTingkatPrestasi.includes(tingkat_prestasi)) { const errorMsg = `Baris ${i+2}: Tingkat prestasi tidak valid "${tingkat_prestasi}" untuk NIM ${nim}. Harus salah satu dari: ${validTingkatPrestasi.join(', ')}`; errors.push(errorMsg); continue; } const datePattern = /^\d{4}-\d{2}-\d{2}$/; if (!datePattern.test(tanggal_prestasi)) { try { const ddmmyyyyPattern = /^(\d{1,2})-(\d{1,2})-(\d{4})$/; const ddmmyyyyMatch = tanggal_prestasi.match(ddmmyyyyPattern); const ddmmyyyySlashPattern = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/; const ddmmyyyySlashMatch = tanggal_prestasi.match(ddmmyyyySlashPattern); if (ddmmyyyyMatch) { const day = ddmmyyyyMatch[1].padStart(2, '0'); const month = ddmmyyyyMatch[2].padStart(2, '0'); const year = ddmmyyyyMatch[3]; if (parseInt(year) < 1900 || parseInt(year) > 2100) { const errorMsg = `Baris ${i+2}: Tahun tidak valid "${year}" untuk NIM ${nim}. Tahun harus antara 1900-2100`; errors.push(errorMsg); continue; } tanggal_prestasi = `${year}-${month}-${day}`; } else if (ddmmyyyySlashMatch) { const day = ddmmyyyySlashMatch[1].padStart(2, '0'); const month = ddmmyyyySlashMatch[2].padStart(2, '0'); const year = ddmmyyyySlashMatch[3]; if (parseInt(year) < 1900 || parseInt(year) > 2100) { const errorMsg = `Baris ${i+2}: Tahun tidak valid "${year}" untuk NIM ${nim}. Tahun harus antara 1900-2100`; errors.push(errorMsg); continue; } tanggal_prestasi = `${year}-${month}-${day}`; } else { const numericValue = Number(tanggal_prestasi); if (!isNaN(numericValue)) { let dateObj; if (numericValue > 60) { const adjustedValue = numericValue - 1; const daysToMs = adjustedValue * 24 * 60 * 60 * 1000; dateObj = new Date(new Date(1899, 11, 30).getTime() + daysToMs); } else { const daysToMs = numericValue * 24 * 60 * 60 * 1000; dateObj = new Date(new Date(1899, 11, 30).getTime() + daysToMs); } if (isValidDate(dateObj)) { tanggal_prestasi = dateObj.toISOString().split('T')[0]; } else { const errorMsg = `Baris ${i+2}: Format tanggal tidak valid "${tanggal_prestasi}" untuk NIM ${nim}. Tahun harus antara 1900-2100`; errors.push(errorMsg); continue; } } else { const dateObj = new Date(tanggal_prestasi); if (!isValidDate(dateObj)) { const errorMsg = `Baris ${i+2}: Format tanggal tidak valid "${tanggal_prestasi}" untuk NIM ${nim}. Gunakan format DD-MM-YYYY, DD/MM/YYYY, atau YYYY-MM-DD`; errors.push(errorMsg); continue; } tanggal_prestasi = dateObj.toISOString().split('T')[0]; } } } catch (e) { const errorMsg = `Baris ${i+2}: Format tanggal tidak valid "${tanggal_prestasi}" untuk NIM ${nim}. Gunakan format DD-MM-YYYY, DD/MM/YYYY, atau YYYY-MM-DD`; errors.push(errorMsg); continue; } } validData.push({ nim, jenis_prestasi, nama_prestasi, tingkat_prestasi, peringkat, tanggal_prestasi, keterangan: null }); } catch (error) { const errorMsg = `Baris ${i+2}: Error memproses data - ${(error as Error).message}`; errors.push(errorMsg); } } return { validData, errors }; } function normalizeJenisPrestasi(value: string): string { const lowerValue = value.toLowerCase(); if (['akademik', 'academic', 'akademis', 'a'].includes(lowerValue)) { return 'Akademik'; } if (['non-akademik', 'non akademik', 'nonakademik', 'non academic', 'na', 'n'].includes(lowerValue)) { return 'Non-Akademik'; } return value; } function normalizeTingkatPrestasi(value: string): string { const lowerValue = value.toLowerCase(); if (['kabupaten', 'kota', 'city', 'kab', 'k'].includes(lowerValue)) { return 'Kabupaten'; } if (['provinsi', 'province', 'prov', 'p'].includes(lowerValue)) { return 'Provinsi'; } if (['nasional', 'national', 'nas', 'n'].includes(lowerValue)) { return 'Nasional'; } if (['internasional', 'international', 'int', 'i'].includes(lowerValue)) { return 'Internasional'; } return value; } function isValidDate(date: Date): boolean { return !isNaN(date.getTime()) && date.getFullYear() >= 1900 && date.getFullYear() <= 2100; } async function insertDataToDatabase(data: any[]) { let imported = 0; let errorCount = 0; const errorMessages: string[] = []; console.log('=== DEBUG: Starting prestasi data insertion process ==='); console.log(`Total data items to process: ${data.length}`); console.log('Sample data items:', data.slice(0, 3)); // First, validate all NIMs exist before processing const uniqueNims = [...new Set(data.map(item => item.nim))]; console.log(`Unique NIMs found: ${uniqueNims.length}`); console.log('Unique NIMs:', uniqueNims); const nimValidationMap = new Map(); // Batch check all NIMs for existence console.log('=== DEBUG: Starting NIM validation ==='); for (const nim of uniqueNims) { try { console.log(`Checking NIM: ${nim}`); const { data: mahasiswaData, error: checkError } = await supabase .from('mahasiswa') .select('id_mahasiswa, nama') .eq('nim', nim) .single(); if (checkError || !mahasiswaData) { console.log(`❌ NIM ${nim}: NOT FOUND in database`); console.log(`Error details:`, checkError); nimValidationMap.set(nim, { exists: false, error: 'Mahasiswa dengan NIM ini tidak ditemukan dalam database' }); } else { console.log(`✅ NIM ${nim}: FOUND - ID: ${mahasiswaData.id_mahasiswa}, Nama: ${mahasiswaData.nama}`); nimValidationMap.set(nim, { exists: true, id_mahasiswa: mahasiswaData.id_mahasiswa, nama: mahasiswaData.nama }); } } catch (error) { console.log(`❌ NIM ${nim}: ERROR during validation`); console.log(`Error details:`, error); nimValidationMap.set(nim, { exists: false, error: `Error checking NIM: ${(error as Error).message}` }); } } console.log('=== DEBUG: NIM validation results ==='); console.log('Validation map:', Object.fromEntries(nimValidationMap)); // Process each data item console.log('=== DEBUG: Starting prestasi data processing ==='); for (const item of data) { try { console.log(`\n--- Processing prestasi item: NIM ${item.nim} ---`); console.log('Item data:', item); const nimValidation = nimValidationMap.get(item.nim); console.log('NIM validation result:', nimValidation); if (!nimValidation || !nimValidation.exists) { errorCount++; const errorMsg = nimValidation?.error || `NIM ${item.nim}: Mahasiswa dengan NIM ini tidak ditemukan dalam database`; console.log(`❌ Skipping item - ${errorMsg}`); errorMessages.push(errorMsg); continue; } console.log(`✅ NIM ${item.nim} is valid, proceeding with prestasi check/insert`); // Check if prestasi already exists for this mahasiswa console.log(`Checking existing prestasi for mahasiswa ID: ${nimValidation.id_mahasiswa}, nama_prestasi: ${item.nama_prestasi}, tanggal: ${item.tanggal_prestasi}`); const { data: existingPrestasi, error: prestasiCheckError } = await supabase .from('prestasi_mahasiswa') .select('id_prestasi') .eq('id_mahasiswa', nimValidation.id_mahasiswa) .eq('nama_prestasi', item.nama_prestasi) .eq('tanggal_prestasi', item.tanggal_prestasi) .single(); if (prestasiCheckError && prestasiCheckError.code !== 'PGRST116') { console.log(`❌ Error checking existing prestasi:`, prestasiCheckError); } if (existingPrestasi) { console.log(`📝 Updating existing prestasi (ID: ${existingPrestasi.id_prestasi})`); // Update existing prestasi const { error: updateError } = await supabase .from('prestasi_mahasiswa') .update({ jenis_prestasi: item.jenis_prestasi, tingkat_prestasi: item.tingkat_prestasi, peringkat: item.peringkat, keterangan: item.keterangan || null }) .eq('id_prestasi', existingPrestasi.id_prestasi); if (updateError) { errorCount++; const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal memperbarui prestasi: ${updateError.message}`; console.log(`❌ Update failed: ${errorMsg}`); errorMessages.push(errorMsg); continue; } else { console.log(`✅ Prestasi updated successfully`); } } else { console.log(`📝 Inserting new prestasi for mahasiswa ID: ${nimValidation.id_mahasiswa}`); // Insert new prestasi const { error: insertError } = await supabase .from('prestasi_mahasiswa') .insert({ id_mahasiswa: nimValidation.id_mahasiswa, jenis_prestasi: item.jenis_prestasi, nama_prestasi: item.nama_prestasi, tingkat_prestasi: item.tingkat_prestasi, peringkat: item.peringkat, tanggal_prestasi: item.tanggal_prestasi, keterangan: item.keterangan || null }); if (insertError) { errorCount++; const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal menyimpan prestasi: ${insertError.message}`; console.log(`❌ Insert failed: ${errorMsg}`); errorMessages.push(errorMsg); continue; } else { console.log(`✅ Prestasi inserted successfully`); } } imported++; console.log(`✅ Item processed successfully. Imported count: ${imported}`); } catch (error) { console.error(`❌ Error processing record for NIM ${item.nim}:`, error); errorCount++; errorMessages.push(`NIM ${item.nim}: Terjadi kesalahan: ${(error as Error).message}`); } } console.log('=== DEBUG: Final results ==='); console.log(`Total imported: ${imported}`); console.log(`Total errors: ${errorCount}`); console.log(`Error messages:`, errorMessages); return { imported, errorCount, errorMessages }; }