258 lines
7.2 KiB
TypeScript
258 lines
7.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import supabase from '@/lib/db';
|
|
import * as XLSX from 'xlsx';
|
|
|
|
interface MataKuliahUpload {
|
|
kode_mk: string;
|
|
nama_mk: string;
|
|
sks: number;
|
|
semester: number;
|
|
jenis_mk: 'Wajib' | 'Pilihan Wajib' | 'Pilihan';
|
|
kode_prasyarat?: string;
|
|
}
|
|
|
|
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: 'No file uploaded' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate file type
|
|
const validTypes = [
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'text/csv'
|
|
];
|
|
|
|
if (!validTypes.includes(file.type)) {
|
|
return NextResponse.json(
|
|
{ message: 'Invalid file type. Please upload Excel (.xlsx, .xls) or CSV (.csv) file' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate file size (10MB max)
|
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
if (file.size > maxSize) {
|
|
return NextResponse.json(
|
|
{ message: 'File size too large. Maximum size is 10MB' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Read file content
|
|
const buffer = await file.arrayBuffer();
|
|
let data: any[] = [];
|
|
|
|
if (file.type === 'text/csv') {
|
|
// 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' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
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 = {};
|
|
headers.forEach((header, index) => {
|
|
row[header] = values[index] || '';
|
|
});
|
|
data.push(row);
|
|
}
|
|
} else {
|
|
// Handle Excel file
|
|
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
|
const sheetName = workbook.SheetNames[0];
|
|
const worksheet = workbook.Sheets[sheetName];
|
|
data = XLSX.utils.sheet_to_json(worksheet);
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
return NextResponse.json(
|
|
{ message: 'No data found in file' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate required columns
|
|
const requiredColumns = ['kode_mk', 'nama_mk', 'sks', 'semester', 'jenis_mk'];
|
|
const firstRow = data[0];
|
|
const missingColumns = requiredColumns.filter(col => !(col in firstRow));
|
|
|
|
if (missingColumns.length > 0) {
|
|
return NextResponse.json(
|
|
{ message: `Missing required columns: ${missingColumns.join(', ')}` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Process and validate data
|
|
const processedData: MataKuliahUpload[] = [];
|
|
const errors: string[] = [];
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
const row = data[i];
|
|
const rowNum = i + 2; // +2 because Excel rows start from 1 and we skip header
|
|
|
|
try {
|
|
// Validate and convert data types
|
|
const sks = parseInt(row.sks);
|
|
const semester = parseInt(row.semester);
|
|
|
|
if (isNaN(sks) || sks <= 0 || sks > 6) {
|
|
errors.push(`Row ${rowNum}: SKS must be a number between 1-6`);
|
|
continue;
|
|
}
|
|
|
|
if (isNaN(semester) || semester <= 0 || semester > 8) {
|
|
errors.push(`Row ${rowNum}: Semester must be a number between 1-8`);
|
|
continue;
|
|
}
|
|
|
|
// Validate jenis_mk
|
|
const validJenisMK = ['Wajib', 'Pilihan Wajib', 'Pilihan'];
|
|
if (!validJenisMK.includes(row.jenis_mk)) {
|
|
errors.push(`Row ${rowNum}: jenis_mk must be one of: ${validJenisMK.join(', ')}`);
|
|
continue;
|
|
}
|
|
|
|
// Validate required fields
|
|
if (!row.kode_mk || !row.nama_mk) {
|
|
errors.push(`Row ${rowNum}: kode_mk and nama_mk are required`);
|
|
continue;
|
|
}
|
|
|
|
processedData.push({
|
|
kode_mk: row.kode_mk.toString().trim(),
|
|
nama_mk: row.nama_mk.toString().trim(),
|
|
sks,
|
|
semester,
|
|
jenis_mk: row.jenis_mk as 'Wajib' | 'Pilihan Wajib' | 'Pilihan',
|
|
kode_prasyarat: row.kode_prasyarat ? row.kode_prasyarat.toString().trim() : undefined
|
|
});
|
|
} catch (error) {
|
|
errors.push(`Row ${rowNum}: Invalid data format`);
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return NextResponse.json(
|
|
{
|
|
message: 'Validation errors found',
|
|
errors: errors.slice(0, 10) // Limit to first 10 errors
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Get existing mata kuliah for duplicate check and prasyarat validation
|
|
const { data: existingMK, error: fetchError } = await supabase
|
|
.from('mata_kuliah')
|
|
.select('kode_mk, id_mk');
|
|
|
|
if (fetchError) {
|
|
console.error('Error fetching existing mata kuliah:', fetchError);
|
|
return NextResponse.json(
|
|
{ message: 'Failed to validate data' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const existingCodes = new Set(existingMK?.map(mk => mk.kode_mk) || []);
|
|
const codeToIdMap = new Map(existingMK?.map(mk => [mk.kode_mk, mk.id_mk]) || []);
|
|
|
|
// Process data for insertion
|
|
const insertData: any[] = [];
|
|
const duplicates: string[] = [];
|
|
|
|
for (const mk of processedData) {
|
|
// Check for duplicates
|
|
if (existingCodes.has(mk.kode_mk)) {
|
|
duplicates.push(mk.kode_mk);
|
|
continue;
|
|
}
|
|
|
|
// Resolve prasyarat ID
|
|
let id_prasyarat = null;
|
|
if (mk.kode_prasyarat) {
|
|
id_prasyarat = codeToIdMap.get(mk.kode_prasyarat);
|
|
if (!id_prasyarat) {
|
|
errors.push(`Prasyarat ${mk.kode_prasyarat} not found for ${mk.kode_mk}`);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
insertData.push({
|
|
kode_mk: mk.kode_mk,
|
|
nama_mk: mk.nama_mk,
|
|
sks: mk.sks,
|
|
semester: mk.semester,
|
|
jenis_mk: mk.jenis_mk,
|
|
id_prasyarat
|
|
});
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return NextResponse.json(
|
|
{
|
|
message: 'Prasyarat validation errors',
|
|
errors: errors.slice(0, 10)
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Insert data to database
|
|
let successCount = 0;
|
|
if (insertData.length > 0) {
|
|
const { error: insertError } = await supabase
|
|
.from('mata_kuliah')
|
|
.insert(insertData);
|
|
|
|
if (insertError) {
|
|
console.error('Error inserting mata kuliah:', insertError);
|
|
return NextResponse.json(
|
|
{ message: 'Failed to insert data to database' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
successCount = insertData.length;
|
|
}
|
|
|
|
// Prepare response
|
|
const response: any = {
|
|
message: 'Upload completed',
|
|
successCount,
|
|
totalRows: processedData.length
|
|
};
|
|
|
|
if (duplicates.length > 0) {
|
|
response.duplicates = duplicates;
|
|
response.duplicateCount = duplicates.length;
|
|
}
|
|
|
|
return NextResponse.json(response);
|
|
|
|
} catch (error) {
|
|
console.error('Error processing upload:', error);
|
|
return NextResponse.json(
|
|
{ message: 'Internal server error' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|