revisi ini

This commit is contained in:
Randa Firman Putra
2026-02-04 02:09:05 +07:00
parent 1e30a9ccfc
commit 3797daa557
3 changed files with 139 additions and 47 deletions

View File

@@ -78,11 +78,6 @@ export async function GET(request: NextRequest) {
mata_kuliah!inner(kode_mk, nama_mk) 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 // Add semester filter if provided
if (semester && semester !== 'all') { if (semester && semester !== 'all') {
query = query.eq('semester', parseInt(semester)); query = query.eq('semester', parseInt(semester));
@@ -94,7 +89,7 @@ export async function GET(request: NextRequest) {
} }
// Add order by // 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; const { data, error } = await query;
@@ -104,7 +99,7 @@ export async function GET(request: NextRequest) {
} }
// Transformasi data untuk meratakan field yang di-join // Transformasi data untuk meratakan field yang di-join
const transformedData = data.map((item: any) => ({ let transformedData = data.map((item: any) => ({
...item, ...item,
nim: item.mahasiswa?.nim || '', nim: item.mahasiswa?.nim || '',
nama: item.mahasiswa?.nama || '', nama: item.mahasiswa?.nama || '',
@@ -115,6 +110,27 @@ export async function GET(request: NextRequest) {
return rest; 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); return NextResponse.json(transformedData);
} }
} catch (error) { } catch (error) {
@@ -230,7 +246,7 @@ export async function POST(request: NextRequest) {
} }
return NextResponse.json( return NextResponse.json(
{ {
message: 'Nilai mahasiswa berhasil ditambahkan', message: 'Nilai mahasiswa berhasil ditambahkan',
id: data.id_nilai, id: data.id_nilai,
mahasiswa: mahasiswa.nama, mahasiswa: mahasiswa.nama,

View File

@@ -54,7 +54,7 @@ export async function POST(request: NextRequest) {
// Handle CSV file // Handle CSV file
const text = new TextDecoder().decode(buffer); const text = new TextDecoder().decode(buffer);
const lines = text.split('\n').filter(line => line.trim()); const lines = text.split('\n').filter(line => line.trim());
if (lines.length < 2) { if (lines.length < 2) {
return NextResponse.json( return NextResponse.json(
{ message: 'File must contain at least header and one data row' }, { 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, '')); const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
for (let i = 1; i < lines.length; i++) { for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, '')); const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
const row: any = {}; const row: any = {};
@@ -111,33 +111,33 @@ export async function POST(request: NextRequest) {
// Further normalize data with column mappings // Further normalize data with column mappings
const finalData = normalizedData.map(row => { const finalData = normalizedData.map(row => {
const finalRow: any = {}; const finalRow: any = {};
Object.keys(columnMappings).forEach(standardKey => { Object.keys(columnMappings).forEach(standardKey => {
const variations = columnMappings[standardKey as keyof typeof columnMappings]; const variations = columnMappings[standardKey as keyof typeof columnMappings];
let value = null; let value = null;
// Try to find the value using different variations // Try to find the value using different variations
for (const variation of variations) { for (const variation of variations) {
const normalizedVariation = variation.toLowerCase() const normalizedVariation = variation.toLowerCase()
.replace(/\s+/g, '_') .replace(/\s+/g, '_')
.replace(/[^a-z0-9_]/g, ''); .replace(/[^a-z0-9_]/g, '');
if (row[normalizedVariation] !== undefined && row[normalizedVariation] !== null && row[normalizedVariation] !== '') { if (row[normalizedVariation] !== undefined && row[normalizedVariation] !== null && row[normalizedVariation] !== '') {
value = row[normalizedVariation]; value = row[normalizedVariation];
break; break;
} }
} }
finalRow[standardKey] = value; finalRow[standardKey] = value;
}); });
return finalRow; return finalRow;
}); });
// Validate required columns // Validate required columns
const requiredColumns = ['nim', 'kode_mk', 'semester', 'nilai_huruf', 'nilai_angka']; const requiredColumns = ['nim', 'kode_mk', 'semester', 'nilai_huruf', 'nilai_angka'];
const firstRow = finalData[0]; const firstRow = finalData[0];
const missingColumns = requiredColumns.filter(col => const missingColumns = requiredColumns.filter(col =>
firstRow[col] === undefined || firstRow[col] === null || firstRow[col] === '' firstRow[col] === undefined || firstRow[col] === null || firstRow[col] === ''
); );
@@ -162,7 +162,7 @@ export async function POST(request: NextRequest) {
try { try {
// Validate and convert data types // Validate and convert data types
const semester = parseInt(row.semester); const semester = parseInt(row.semester);
// Handle decimal comma (Indonesian format) and convert to dot // Handle decimal comma (Indonesian format) and convert to dot
const nilaiAngkaStr = row.nilai_angka.toString().replace(',', '.'); const nilaiAngkaStr = row.nilai_angka.toString().replace(',', '.');
const nilai_angka = parseFloat(nilaiAngkaStr); const nilai_angka = parseFloat(nilaiAngkaStr);
@@ -207,7 +207,7 @@ export async function POST(request: NextRequest) {
if (errors.length > 0) { if (errors.length > 0) {
return NextResponse.json( return NextResponse.json(
{ {
message: 'Validation errors found', message: 'Validation errors found',
errors: errors.slice(0, 10) // Limit to first 10 errors 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 nimToIdMap = new Map(existingMahasiswa?.map(m => [m.nim, m.id_mahasiswa]) || []);
const kodeMKToIdMap = new Map(existingMataKuliah?.map(mk => [mk.kode_mk, mk.id_mk]) || []); 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 insertData: any[] = [];
const validationErrors: string[] = []; const validationErrors: string[] = [];
@@ -271,7 +271,7 @@ export async function POST(request: NextRequest) {
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
return NextResponse.json( return NextResponse.json(
{ {
message: 'Data validation errors', message: 'Data validation errors',
errors: validationErrors.slice(0, 10) errors: validationErrors.slice(0, 10)
}, },
@@ -279,26 +279,88 @@ export async function POST(request: NextRequest) {
); );
} }
// Insert data to database // Fetch existing nilai_mahasiswa to determine which to update vs insert
let successCount = 0; const { data: existingNilai, error: fetchNilaiError } = await supabase
if (insertData.length > 0) { .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 const { error: insertError } = await supabase
.from('nilai_mahasiswa') .from('nilai_mahasiswa')
.insert(insertData); .insert(dataToInsert);
if (insertError) { if (insertError) {
console.error('Insert error:', insertError);
return NextResponse.json( return NextResponse.json(
{ message: 'Failed to insert data to database' }, { message: 'Failed to insert data to database', error: insertError.message },
{ status: 500 } { 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({ return NextResponse.json({
message: 'Upload completed', message: 'Upload completed',
successCount, inserted: insertCount,
updated: updateCount,
totalProcessed: insertCount + updateCount,
totalRows: processedData.length totalRows: processedData.length
}); });

View File

@@ -3,11 +3,11 @@
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
DialogClose DialogClose
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
@@ -34,12 +34,12 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv' 'text/csv'
]; ];
if (!validTypes.includes(file.type)) { if (!validTypes.includes(file.type)) {
showError("Error!", "File harus berformat Excel (.xlsx, .xls) atau CSV (.csv)"); showError("Error!", "File harus berformat Excel (.xlsx, .xls) atau CSV (.csv)");
return; return;
} }
setSelectedFile(file); setSelectedFile(file);
} }
}; };
@@ -53,17 +53,17 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
try { try {
setIsUploading(true); setIsUploading(true);
const formData = new FormData(); const formData = new FormData();
formData.append('file', selectedFile); formData.append('file', selectedFile);
const response = await fetch('/api/keloladata/data-nilai-mahasiswa/upload', { const response = await fetch('/api/keloladata/data-nilai-mahasiswa/upload', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
const result = await response.json(); const result = await response.json();
if (!response.ok) { if (!response.ok) {
let errorMessage = result.message || 'Upload failed'; let errorMessage = result.message || 'Upload failed';
if (result.errors && result.errors.length > 0) { if (result.errors && result.errors.length > 0) {
@@ -71,12 +71,26 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
} }
throw new Error(errorMessage); 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(); onUploadSuccess();
setIsDialogOpen(false); setIsDialogOpen(false);
setSelectedFile(null); setSelectedFile(null);
// Reset file input // Reset file input
const fileInput = document.getElementById('file-upload-nilai') as HTMLInputElement; const fileInput = document.getElementById('file-upload-nilai') as HTMLInputElement;
if (fileInput) { if (fileInput) {
@@ -105,7 +119,7 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
Import Data Nilai Mahasiswa Import Data Nilai Mahasiswa
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="file-upload-nilai" className="text-sm font-medium"> <label htmlFor="file-upload-nilai" className="text-sm font-medium">
@@ -122,7 +136,7 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
Format yang didukung: .xlsx, .xls, .csv (Max: 10MB) Format yang didukung: .xlsx, .xls, .csv (Max: 10MB)
</p> </p>
</div> </div>
{selectedFile && ( {selectedFile && (
<div className="p-3 bg-muted rounded-md"> <div className="p-3 bg-muted rounded-md">
<p className="text-sm font-medium">File terpilih:</p> <p className="text-sm font-medium">File terpilih:</p>
@@ -132,7 +146,7 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
</p> </p>
</div> </div>
)} )}
<div className="border-t pt-4"> <div className="border-t pt-4">
<h4 className="text-sm font-medium mb-2">Format File:</h4> <h4 className="text-sm font-medium mb-2">Format File:</h4>
<div className="text-xs text-muted-foreground space-y-1"> <div className="text-xs text-muted-foreground space-y-1">
@@ -145,15 +159,15 @@ export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFile
</div> </div>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button variant="outline"> <Button variant="outline">
Batal Batal
</Button> </Button>
</DialogClose> </DialogClose>
<Button <Button
onClick={handleUpload} onClick={handleUpload}
disabled={!selectedFile || isUploading} disabled={!selectedFile || isUploading}
> >
{isUploading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {isUploading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}