Files
portaldata/app/api/keloladata/data-prestasi-mahasiswa/upload/route.ts
Randa Firman Putra 6d86e1ca2f Change Alur Aplikasi
2025-07-14 15:07:33 +07:00

470 lines
17 KiB
TypeScript

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