From 3797daa5570dd50e2af722b45ccd2f19a665daf2 Mon Sep 17 00:00:00 2001 From: Randa Firman Putra Date: Wed, 4 Feb 2026 02:09:05 +0700 Subject: [PATCH] revisi ini --- .../keloladata/data-nilai-mahasiswa/route.ts | 32 ++++-- .../data-nilai-mahasiswa/upload/route.ts | 100 ++++++++++++++---- .../datatable/upload-file-nilai-mahasiswa.tsx | 54 ++++++---- 3 files changed, 139 insertions(+), 47 deletions(-) diff --git a/app/api/keloladata/data-nilai-mahasiswa/route.ts b/app/api/keloladata/data-nilai-mahasiswa/route.ts index f97cbd4..5b1f5a4 100644 --- a/app/api/keloladata/data-nilai-mahasiswa/route.ts +++ b/app/api/keloladata/data-nilai-mahasiswa/route.ts @@ -78,11 +78,6 @@ export async function GET(request: NextRequest) { 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)); @@ -94,7 +89,7 @@ export async function GET(request: NextRequest) { } // Add order by - query = query.order('semester', { ascending: true }).order('mahasiswa(nim)', { ascending: true }); + query = query.order('semester', { ascending: true }); const { data, error } = await query; @@ -104,7 +99,7 @@ export async function GET(request: NextRequest) { } // Transformasi data untuk meratakan field yang di-join - const transformedData = data.map((item: any) => ({ + let transformedData = data.map((item: any) => ({ ...item, nim: item.mahasiswa?.nim || '', nama: item.mahasiswa?.nama || '', @@ -115,6 +110,27 @@ export async function GET(request: NextRequest) { return rest; }); + // Client-side search filtering (untuk joined tables) + if (search) { + const searchLower = search.toLowerCase(); + transformedData = transformedData.filter((item: any) => { + return ( + item.nim?.toLowerCase().includes(searchLower) || + item.nama?.toLowerCase().includes(searchLower) || + item.kode_mk?.toLowerCase().includes(searchLower) || + item.nama_mk?.toLowerCase().includes(searchLower) + ); + }); + } + + // Sort by nim after filtering + transformedData.sort((a: any, b: any) => { + if (a.semester !== b.semester) { + return a.semester - b.semester; + } + return a.nim.localeCompare(b.nim); + }); + return NextResponse.json(transformedData); } } catch (error) { @@ -230,7 +246,7 @@ export async function POST(request: NextRequest) { } return NextResponse.json( - { + { message: 'Nilai mahasiswa berhasil ditambahkan', id: data.id_nilai, mahasiswa: mahasiswa.nama, diff --git a/app/api/keloladata/data-nilai-mahasiswa/upload/route.ts b/app/api/keloladata/data-nilai-mahasiswa/upload/route.ts index 231ce38..9481672 100644 --- a/app/api/keloladata/data-nilai-mahasiswa/upload/route.ts +++ b/app/api/keloladata/data-nilai-mahasiswa/upload/route.ts @@ -54,7 +54,7 @@ export async function POST(request: NextRequest) { // 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' }, @@ -63,7 +63,7 @@ export async function POST(request: NextRequest) { } 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 = {}; @@ -111,33 +111,33 @@ export async function POST(request: NextRequest) { // 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 => + const missingColumns = requiredColumns.filter(col => firstRow[col] === undefined || firstRow[col] === null || firstRow[col] === '' ); @@ -162,7 +162,7 @@ export async function POST(request: NextRequest) { 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); @@ -207,7 +207,7 @@ export async function POST(request: NextRequest) { if (errors.length > 0) { return NextResponse.json( - { + { message: 'Validation errors found', errors: errors.slice(0, 10) // Limit to first 10 errors }, @@ -241,7 +241,7 @@ export async function POST(request: NextRequest) { 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 + // Process data for insertion/update const insertData: any[] = []; const validationErrors: string[] = []; @@ -271,7 +271,7 @@ export async function POST(request: NextRequest) { if (validationErrors.length > 0) { return NextResponse.json( - { + { message: 'Data validation errors', errors: validationErrors.slice(0, 10) }, @@ -279,26 +279,88 @@ export async function POST(request: NextRequest) { ); } - // Insert data to database - let successCount = 0; - if (insertData.length > 0) { + // Fetch existing nilai_mahasiswa to determine which to update vs insert + const { data: existingNilai, error: fetchNilaiError } = await supabase + .from('nilai_mahasiswa') + .select('id_nilai, id_mahasiswa, id_mk'); + + if (fetchNilaiError) { + return NextResponse.json( + { message: 'Failed to fetch existing nilai data' }, + { status: 500 } + ); + } + + // Create map of existing nilai: "id_mahasiswa-id_mk" -> id_nilai + const existingNilaiMap = new Map( + (existingNilai || []).map(n => [`${n.id_mahasiswa}-${n.id_mk}`, n.id_nilai]) + ); + + // Separate data into updates and inserts + const dataToUpdate: any[] = []; + const dataToInsert: any[] = []; + + for (const data of insertData) { + const key = `${data.id_mahasiswa}-${data.id_mk}`; + const existingId = existingNilaiMap.get(key); + + if (existingId) { + // Data exists, prepare for update + dataToUpdate.push({ + id_nilai: existingId, + ...data, + updated_at: new Date().toISOString() + }); + } else { + // Data doesn't exist, prepare for insert + dataToInsert.push(data); + } + } + + let insertCount = 0; + let updateCount = 0; + + // Perform batch insert + if (dataToInsert.length > 0) { const { error: insertError } = await supabase .from('nilai_mahasiswa') - .insert(insertData); + .insert(dataToInsert); if (insertError) { + console.error('Insert error:', insertError); return NextResponse.json( - { message: 'Failed to insert data to database' }, + { message: 'Failed to insert data to database', error: insertError.message }, { status: 500 } ); } - successCount = insertData.length; + insertCount = dataToInsert.length; + } + + // Perform batch update (one by one because Supabase doesn't support batch update easily) + if (dataToUpdate.length > 0) { + for (const data of dataToUpdate) { + const { id_nilai, ...updateFields } = data; + + const { error: updateError } = await supabase + .from('nilai_mahasiswa') + .update(updateFields) + .eq('id_nilai', id_nilai); + + if (updateError) { + console.error('Update error:', updateError); + // Continue with other updates even if one fails + } else { + updateCount++; + } + } } return NextResponse.json({ message: 'Upload completed', - successCount, + inserted: insertCount, + updated: updateCount, + totalProcessed: insertCount + updateCount, totalRows: processedData.length }); diff --git a/components/datatable/upload-file-nilai-mahasiswa.tsx b/components/datatable/upload-file-nilai-mahasiswa.tsx index 3976f8c..2f3c21c 100644 --- a/components/datatable/upload-file-nilai-mahasiswa.tsx +++ b/components/datatable/upload-file-nilai-mahasiswa.tsx @@ -3,11 +3,11 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog"; @@ -34,12 +34,12 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/csv' ]; - + if (!validTypes.includes(file.type)) { showError("Error!", "File harus berformat Excel (.xlsx, .xls) atau CSV (.csv)"); return; } - + setSelectedFile(file); } }; @@ -53,17 +53,17 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile try { setIsUploading(true); - + const formData = new FormData(); formData.append('file', selectedFile); - + const response = await fetch('/api/keloladata/data-nilai-mahasiswa/upload', { method: 'POST', body: formData, }); - + const result = await response.json(); - + if (!response.ok) { let errorMessage = result.message || 'Upload failed'; if (result.errors && result.errors.length > 0) { @@ -71,12 +71,26 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile } throw new Error(errorMessage); } - - showSuccess("Berhasil!", `${result.successCount} nilai mahasiswa berhasil diimport`); + + // Create success message based on insert and update counts + const inserted = result.inserted || 0; + const updated = result.updated || 0; + const total = inserted + updated; + + let successMessage = `${total} nilai mahasiswa berhasil diproses`; + if (inserted > 0 && updated > 0) { + successMessage += ` (${inserted} ditambahkan, ${updated} diperbarui)`; + } else if (inserted > 0) { + successMessage += ` (${inserted} ditambahkan)`; + } else if (updated > 0) { + successMessage += ` (${updated} diperbarui)`; + } + + showSuccess("Berhasil!", successMessage); onUploadSuccess(); setIsDialogOpen(false); setSelectedFile(null); - + // Reset file input const fileInput = document.getElementById('file-upload-nilai') as HTMLInputElement; if (fileInput) { @@ -105,7 +119,7 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile Import Data Nilai Mahasiswa - +
- + {selectedFile && (

File terpilih:

@@ -132,7 +146,7 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile

)} - +

Format File:

@@ -145,15 +159,15 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
- + -