Change Alur Aplikasi
This commit is contained in:
@@ -10,7 +10,7 @@ export async function GET() {
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ error: 'Unauthorized', isAuthenticated: false },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
@@ -21,33 +21,58 @@ export async function GET() {
|
||||
new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key')
|
||||
);
|
||||
|
||||
// Check if token is expired
|
||||
if (payload.exp && payload.exp * 1000 < Date.now()) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Token expired', isAuthenticated: false },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user data from user_app table
|
||||
const { data: user, error } = await supabase
|
||||
.from('user_app')
|
||||
.select('id_user, nim, username, role')
|
||||
.select('id_user, nim, username, nip, role_user')
|
||||
.eq('id_user', payload.id)
|
||||
.single();
|
||||
|
||||
if (error || !user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User not found' },
|
||||
{ error: 'User not found', isAuthenticated: false },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
id: user.id_user,
|
||||
nim: user.nim,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
nip: user.nip,
|
||||
role: user.role_user
|
||||
},
|
||||
session: {
|
||||
expiresAt: payload.exp ? new Date(payload.exp * 1000).toISOString() : null,
|
||||
issuedAt: payload.iat ? new Date(payload.iat * 1000).toISOString() : null
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Auth check error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ error: 'Unauthorized', isAuthenticated: false },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle OPTIONS request for CORS
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json({}, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,198 +1,105 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { SignJWT } from 'jose';
|
||||
|
||||
interface User {
|
||||
id_user: number;
|
||||
username?: string;
|
||||
nip?: string;
|
||||
password: string;
|
||||
role_user: string;
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
||||
// Test database connection first
|
||||
try {
|
||||
const { data: testData, error: testError } = await supabase
|
||||
.from('user_app')
|
||||
.select('count')
|
||||
.limit(1);
|
||||
|
||||
if (testError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tidak dapat terhubung ke database' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (dbError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tidak dapat terhubung ke database' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const { username, nip, password, role } = body;
|
||||
const { nip, username, password, role } = body;
|
||||
|
||||
// Validate input based on role
|
||||
if (role === 'admin') {
|
||||
if (!username || !password) {
|
||||
// Validate required fields
|
||||
if (!password || !role) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Username dan password harus diisi' },
|
||||
{ status: 400 }
|
||||
{ message: 'Password dan role diperlukan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} else if (role === 'dosen' || role === 'kajur') {
|
||||
if (!nip || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'NIP dan password harus diisi' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
// Validate role
|
||||
if (!['ketuajurusan', 'admin'].includes(role)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Role tidak valid' },
|
||||
{ message: 'Role tidak valid' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get user by username (admin) or NIP (dosen/kajur)
|
||||
let users: User[];
|
||||
try {
|
||||
let query = supabase.from('user_app').select('*');
|
||||
|
||||
if (role === 'admin') {
|
||||
query = query.eq('username', username);
|
||||
} else {
|
||||
query = query.eq('nip', nip);
|
||||
}
|
||||
let query = supabase
|
||||
.from('user_app')
|
||||
.select('*')
|
||||
.eq('role_user', role);
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
// Add specific field filter based on role
|
||||
if (role === 'ketuajurusan') {
|
||||
if (!nip) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Terjadi kesalahan saat memeriksa data pengguna' },
|
||||
{ status: 500 }
|
||||
{ message: 'NIP diperlukan untuk Ketua Jurusan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
query = query.eq('nip', nip);
|
||||
} else if (role === 'admin') {
|
||||
if (!username) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Username diperlukan untuk Admin' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
query = query.eq('username', username);
|
||||
}
|
||||
|
||||
users = data || [];
|
||||
} catch (queryError) {
|
||||
const { data: users, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Database error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Terjadi kesalahan saat memeriksa data pengguna' },
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (users.length === 0) {
|
||||
if (!users || users.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: role === 'admin' ? 'Username atau password salah' : 'NIP atau password salah' },
|
||||
{ message: 'User tidak ditemukan' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
// Check if user role matches
|
||||
if (user.role_user !== role) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Role tidak sesuai' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
let isPasswordValid;
|
||||
try {
|
||||
// For admin, check if password is plain text (not hashed)
|
||||
if (user.role_user === 'admin') {
|
||||
// Check if stored password is plain text (not starting with $2a$ or $2b$)
|
||||
if (!user.password.startsWith('$2a$') && !user.password.startsWith('$2b$')) {
|
||||
// Plain text password - direct comparison
|
||||
isPasswordValid = password === user.password;
|
||||
} else {
|
||||
// Hashed password - use bcrypt
|
||||
isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
}
|
||||
} else {
|
||||
// For dosen/kajur, always use bcrypt (should be hashed)
|
||||
isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
}
|
||||
} catch (bcryptError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Terjadi kesalahan saat memverifikasi password' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{ error: role === 'admin' ? 'Username atau password salah' : 'NIP atau password salah' },
|
||||
{ message: 'Password salah' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create JWT token
|
||||
let token;
|
||||
try {
|
||||
const tokenPayload: any = {
|
||||
id: user.id_user,
|
||||
role: user.role_user
|
||||
};
|
||||
|
||||
// Add username for admin, NIP for dosen/kajur
|
||||
if (user.role_user === 'admin') {
|
||||
tokenPayload.username = user.username;
|
||||
} else {
|
||||
tokenPayload.nip = user.nip;
|
||||
}
|
||||
|
||||
token = await new SignJWT(tokenPayload)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setExpirationTime('24h')
|
||||
.sign(new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key'));
|
||||
} catch (jwtError) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Terjadi kesalahan saat membuat token' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
// Return user data (without password)
|
||||
const { password: _, ...userWithoutPassword } = user;
|
||||
|
||||
// Set cookie
|
||||
const userResponse: any = {
|
||||
id: user.id_user,
|
||||
role: user.role_user
|
||||
};
|
||||
|
||||
// Add username for admin, NIP for dosen/kajur
|
||||
if (user.role_user === 'admin') {
|
||||
userResponse.username = user.username;
|
||||
} else {
|
||||
userResponse.nip = user.nip;
|
||||
}
|
||||
|
||||
// Create response with session cookie
|
||||
const response = NextResponse.json({
|
||||
user: userResponse
|
||||
message: 'Login berhasil',
|
||||
user: userWithoutPassword,
|
||||
});
|
||||
|
||||
response.cookies.set('token', token, {
|
||||
// Set secure session cookie
|
||||
response.cookies.set('user_session', JSON.stringify(userWithoutPassword), {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 // 24 hours
|
||||
maxAge: 24 * 60 * 60, // 24 hours
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error message:', error.message);
|
||||
}
|
||||
console.error('Login error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Terjadi kesalahan saat login' },
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const response = NextResponse.json(
|
||||
{ message: 'Logout berhasil' },
|
||||
{ status: 200 }
|
||||
);
|
||||
const response = NextResponse.json({
|
||||
message: 'Logout berhasil',
|
||||
});
|
||||
|
||||
// Clear the token cookie with additional security options
|
||||
response.cookies.set('token', '', {
|
||||
expires: new Date(0),
|
||||
path: '/',
|
||||
// Clear the session cookie
|
||||
response.cookies.set('user_session', '', {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'lax'
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0, // Expire immediately
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Terjadi kesalahan saat logout' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle OPTIONS request for CORS
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json({}, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
26
app/api/auth/user/route.ts
Normal file
26
app/api/auth/user/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const userSession = request.cookies.get('user_session');
|
||||
|
||||
if (!userSession) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Tidak ada session aktif' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const userData = JSON.parse(userSession.value);
|
||||
|
||||
return NextResponse.json({
|
||||
user: userData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get user error:', error);
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
322
app/api/keloladata/data-beasiswa-mahasiswa/route.ts
Normal file
322
app/api/keloladata/data-beasiswa-mahasiswa/route.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
// GET - Fetch all beasiswa mahasiswa or filter by criteria
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
const id_mahasiswa = searchParams.get('id_mahasiswa');
|
||||
const search = searchParams.get('search');
|
||||
const jenisBeasiswa = searchParams.get('jenis_beasiswa');
|
||||
|
||||
// If ID is provided, fetch specific beasiswa by ID with join
|
||||
if (id) {
|
||||
const { data, error } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.select(`
|
||||
*,
|
||||
mahasiswa!inner(nama, nim)
|
||||
`)
|
||||
.eq('id_beasiswa', id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return NextResponse.json({ message: 'Beasiswa mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the nama and nim fields
|
||||
const transformedData = {
|
||||
...data,
|
||||
nama: data.mahasiswa.nama,
|
||||
nim: data.mahasiswa.nim
|
||||
};
|
||||
delete transformedData.mahasiswa;
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
}
|
||||
|
||||
// If id_mahasiswa is provided, fetch beasiswa for specific student with join
|
||||
if (id_mahasiswa) {
|
||||
const { data, error } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.select(`
|
||||
*,
|
||||
mahasiswa!inner(nama, nim)
|
||||
`)
|
||||
.eq('id_mahasiswa', id_mahasiswa)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the nama and nim fields
|
||||
const transformedData = data.map(item => ({
|
||||
...item,
|
||||
nama: item.mahasiswa.nama,
|
||||
nim: item.mahasiswa.nim
|
||||
})).map(({ mahasiswa, ...rest }) => rest);
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
}
|
||||
|
||||
// Build the query based on filters with join
|
||||
let query = supabase.from('beasiswa_mahasiswa').select(`
|
||||
*,
|
||||
mahasiswa!inner(nama, nim)
|
||||
`);
|
||||
|
||||
// Add search condition if provided
|
||||
if (search) {
|
||||
query = query.or(`mahasiswa.nama.ilike.%${search}%,mahasiswa.nim.ilike.%${search}%,nama_beasiswa.ilike.%${search}%,sumber_beasiswa.ilike.%${search}%`);
|
||||
}
|
||||
|
||||
// Add jenis_beasiswa filter if provided
|
||||
if (jenisBeasiswa) {
|
||||
query = query.eq('jenis_beasiswa', jenisBeasiswa);
|
||||
}
|
||||
|
||||
// Add order by
|
||||
query = query.order('created_at', { ascending: false });
|
||||
|
||||
// Execute the query
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the nama and nim fields
|
||||
const transformedData = data.map(item => ({
|
||||
...item,
|
||||
nama: item.mahasiswa.nama,
|
||||
nim: item.mahasiswa.nim
|
||||
})).map(({ mahasiswa, ...rest }) => rest);
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create a new beasiswa mahasiswa
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
nim,
|
||||
nama_beasiswa,
|
||||
sumber_beasiswa,
|
||||
beasiswa_status,
|
||||
jenis_beasiswa
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!nim || !nama_beasiswa || !sumber_beasiswa || !beasiswa_status || !jenis_beasiswa) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Missing required fields: nim, nama_beasiswa, sumber_beasiswa, beasiswa_status, jenis_beasiswa' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if mahasiswa exists by NIM and get id_mahasiswa
|
||||
const { data: mahasiswaExists, error: checkError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('id_mahasiswa, nama')
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (checkError || !mahasiswaExists) {
|
||||
return NextResponse.json(
|
||||
{ message: `Mahasiswa dengan NIM ${nim} tidak terdaftar dalam database` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate enum values
|
||||
const validStatus = ['Aktif', 'Selesai', 'Dibatalkan'];
|
||||
const validJenisBeasiswa = ['Pemerintah', 'Non-Pemerintah'];
|
||||
|
||||
if (!validStatus.includes(beasiswa_status)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid beasiswa_status value. Must be one of: Aktif, Selesai, Dibatalkan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!validJenisBeasiswa.includes(jenis_beasiswa)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid jenis_beasiswa value. Must be one of: Pemerintah, Non-Pemerintah' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Insert new beasiswa using id_mahasiswa
|
||||
const { data, error } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.insert({
|
||||
id_mahasiswa: mahasiswaExists.id_mahasiswa,
|
||||
nama_beasiswa,
|
||||
sumber_beasiswa,
|
||||
beasiswa_status,
|
||||
jenis_beasiswa
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating beasiswa mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: `Beasiswa berhasil ditambahkan`,
|
||||
id: data.id_beasiswa
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error creating beasiswa mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update an existing beasiswa mahasiswa
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
nim,
|
||||
nama_beasiswa,
|
||||
sumber_beasiswa,
|
||||
beasiswa_status,
|
||||
jenis_beasiswa
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!nim || !nama_beasiswa || !sumber_beasiswa || !beasiswa_status || !jenis_beasiswa) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Missing required fields: nim, nama_beasiswa, sumber_beasiswa, beasiswa_status, jenis_beasiswa' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if beasiswa exists
|
||||
const { data: existing, error: checkError } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.select('*')
|
||||
.eq('id_beasiswa', id)
|
||||
.single();
|
||||
|
||||
if (checkError || !existing) {
|
||||
return NextResponse.json({ message: 'Beasiswa mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if mahasiswa exists by NIM and get id_mahasiswa
|
||||
const { data: mahasiswaExists, error: mahasiswaCheckError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('id_mahasiswa, nama')
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (mahasiswaCheckError || !mahasiswaExists) {
|
||||
return NextResponse.json(
|
||||
{ message: `Mahasiswa dengan NIM ${nim} tidak terdaftar` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate enum values
|
||||
const validStatus = ['Aktif', 'Selesai', 'Dibatalkan'];
|
||||
const validJenisBeasiswa = ['Pemerintah', 'Non-Pemerintah'];
|
||||
|
||||
if (!validStatus.includes(beasiswa_status)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid beasiswa_status value. Must be one of: Aktif, Selesai, Dibatalkan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!validJenisBeasiswa.includes(jenis_beasiswa)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid jenis_beasiswa value. Must be one of: Pemerintah, Non-Pemerintah' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update beasiswa using id_mahasiswa
|
||||
const { error } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.update({
|
||||
id_mahasiswa: mahasiswaExists.id_mahasiswa,
|
||||
nama_beasiswa,
|
||||
sumber_beasiswa,
|
||||
beasiswa_status,
|
||||
jenis_beasiswa
|
||||
})
|
||||
.eq('id_beasiswa', id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating beasiswa mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Beasiswa berhasil diperbarui`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating beasiswa mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Delete a beasiswa mahasiswa
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if beasiswa exists
|
||||
const { data: existing, error: checkError } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.select('id_beasiswa')
|
||||
.eq('id_beasiswa', id)
|
||||
.single();
|
||||
|
||||
if (checkError || !existing) {
|
||||
return NextResponse.json({ message: 'Beasiswa mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete beasiswa
|
||||
const { error } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.delete()
|
||||
.eq('id_beasiswa', id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting beasiswa mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Beasiswa mahasiswa deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting beasiswa mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
374
app/api/keloladata/data-beasiswa-mahasiswa/upload/route.ts
Normal file
374
app/api/keloladata/data-beasiswa-mahasiswa/upload/route.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import * as XLSX from 'xlsx';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get form data from request
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ message: 'File tidak ditemukan' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Process file data based on file type
|
||||
let validData = [];
|
||||
let errors: string[] = [];
|
||||
|
||||
if (file.name.endsWith('.csv') || file.type === 'text/csv') {
|
||||
// Process as CSV
|
||||
const fileContent = await file.text();
|
||||
const result = await processCSVData(fileContent);
|
||||
validData = result.validData;
|
||||
errors = result.errors;
|
||||
} else {
|
||||
// Process as Excel
|
||||
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 });
|
||||
}
|
||||
|
||||
// Insert valid data into the database
|
||||
const { imported, errorCount, errorMessages } = await insertDataToDatabase(validData);
|
||||
|
||||
// Combine all error messages
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process Excel data
|
||||
async function processExcelData(fileBuffer: ArrayBuffer) {
|
||||
try {
|
||||
// Parse Excel file
|
||||
const workbook = XLSX.read(fileBuffer, { type: 'array' });
|
||||
|
||||
// Get first sheet
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
|
||||
// Convert to JSON with proper typing
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
||||
|
||||
if (jsonData.length === 0) {
|
||||
return { validData: [], errors: ['File Excel kosong'] };
|
||||
}
|
||||
|
||||
// Convert Excel data to CSV-like format for processing
|
||||
const headers = jsonData[0].map(h => String(h).toLowerCase());
|
||||
const rows = jsonData.slice(1);
|
||||
|
||||
// Process the data using the common function
|
||||
return processData(headers, rows);
|
||||
} catch (error) {
|
||||
console.error('Error processing Excel data:', error);
|
||||
return { validData: [], errors: [(error as Error).message] };
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process CSV data
|
||||
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'] };
|
||||
}
|
||||
|
||||
// Get headers from first line
|
||||
const headerLine = lines[0].toLowerCase();
|
||||
const headers = headerLine.split(',').map(h => h.trim());
|
||||
|
||||
// Process data rows
|
||||
const rows = lines.slice(1).map(line => line.split(',').map(v => v.trim()));
|
||||
|
||||
return processData(headers, rows);
|
||||
}
|
||||
|
||||
// Common function to process data regardless of source format
|
||||
function processData(headers: string[], rows: any[][]) {
|
||||
// Define expected headers and their possible variations
|
||||
const expectedHeaderMap = {
|
||||
nim: ['nim', 'nomor induk', 'nomor mahasiswa'],
|
||||
nama_beasiswa: ['nama_beasiswa', 'nama beasiswa', 'namabeasiswa', 'beasiswa', 'nama'],
|
||||
sumber_beasiswa: ['sumber_beasiswa', 'sumber beasiswa', 'sumberbeasiswa', 'sumber'],
|
||||
beasiswa_status: ['beasiswa_status', 'status beasiswa', 'statusbeasiswa', 'status'],
|
||||
jenis_beasiswa: ['jenis_beasiswa', 'jenis beasiswa', 'jenisbeasiswa', 'jenis']
|
||||
};
|
||||
|
||||
// Map actual headers to expected headers
|
||||
const headerMap: { [key: string]: number } = {};
|
||||
for (const [expectedHeader, variations] of Object.entries(expectedHeaderMap)) {
|
||||
const index = headers.findIndex(h =>
|
||||
variations.some(variation => h.includes(variation))
|
||||
);
|
||||
if (index !== -1) {
|
||||
headerMap[expectedHeader] = index;
|
||||
}
|
||||
}
|
||||
|
||||
// Check required headers
|
||||
const requiredHeaders = ['nim', 'nama_beasiswa', 'sumber_beasiswa', 'beasiswa_status', 'jenis_beasiswa'];
|
||||
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, Nama Beasiswa, Sumber Beasiswa, Status Beasiswa, dan Jenis Beasiswa.`]
|
||||
};
|
||||
}
|
||||
|
||||
const validData = [];
|
||||
const errors = [];
|
||||
const validStatuses = ['Aktif', 'Selesai', 'Dibatalkan'];
|
||||
const validJenis = ['Pemerintah', 'Non-Pemerintah'];
|
||||
|
||||
// Process data rows
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const values = rows[i];
|
||||
if (!values || values.length === 0) continue;
|
||||
|
||||
try {
|
||||
// Extract values using header map
|
||||
const nim = String(values[headerMap.nim] || '').trim();
|
||||
const nama_beasiswa = String(values[headerMap.nama_beasiswa] || '').trim();
|
||||
const sumber_beasiswa = String(values[headerMap.sumber_beasiswa] || '').trim();
|
||||
let beasiswa_status = String(values[headerMap.beasiswa_status] || '').trim();
|
||||
let jenis_beasiswa = String(values[headerMap.jenis_beasiswa] || '').trim();
|
||||
|
||||
// Validate required fields
|
||||
if (!nim || !nama_beasiswa || !sumber_beasiswa || !beasiswa_status || !jenis_beasiswa) {
|
||||
errors.push(`Baris ${i+2}: Data tidak lengkap (NIM: ${nim || 'kosong'})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize status beasiswa
|
||||
beasiswa_status = normalizeBeasiswaStatus(beasiswa_status);
|
||||
|
||||
// Validate status beasiswa
|
||||
if (!validStatuses.includes(beasiswa_status)) {
|
||||
errors.push(`Baris ${i+2}: Status beasiswa tidak valid "${beasiswa_status}" untuk NIM ${nim}. Harus salah satu dari: ${validStatuses.join(', ')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normalize jenis beasiswa
|
||||
jenis_beasiswa = normalizeJenisBeasiswa(jenis_beasiswa);
|
||||
|
||||
// Validate jenis beasiswa
|
||||
if (!validJenis.includes(jenis_beasiswa)) {
|
||||
errors.push(`Baris ${i+2}: Jenis beasiswa tidak valid "${jenis_beasiswa}" untuk NIM ${nim}. Harus salah satu dari: ${validJenis.join(', ')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to valid data
|
||||
validData.push({
|
||||
nim,
|
||||
nama_beasiswa,
|
||||
sumber_beasiswa,
|
||||
beasiswa_status,
|
||||
jenis_beasiswa
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
errors.push(`Baris ${i+2}: Error memproses data - ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { validData, errors };
|
||||
}
|
||||
|
||||
// Function to normalize beasiswa status values
|
||||
function normalizeBeasiswaStatus(value: string): string {
|
||||
const lowerValue = value.toLowerCase();
|
||||
|
||||
if (['aktif', 'active', 'a'].includes(lowerValue)) {
|
||||
return 'Aktif';
|
||||
}
|
||||
|
||||
if (['selesai', 'complete', 'completed', 's', 'finish', 'finished'].includes(lowerValue)) {
|
||||
return 'Selesai';
|
||||
}
|
||||
|
||||
if (['dibatalkan', 'cancel', 'cancelled', 'canceled', 'batal', 'd', 'c'].includes(lowerValue)) {
|
||||
return 'Dibatalkan';
|
||||
}
|
||||
|
||||
return value; // Return original if no match
|
||||
}
|
||||
|
||||
// Function to normalize jenis beasiswa values
|
||||
function normalizeJenisBeasiswa(value: string): string {
|
||||
const lowerValue = value.toLowerCase();
|
||||
|
||||
if (['pemerintah', 'government', 'p', 'gov'].includes(lowerValue)) {
|
||||
return 'Pemerintah';
|
||||
}
|
||||
|
||||
if (['non-pemerintah', 'non pemerintah', 'nonpemerintah', 'swasta', 'private', 'np', 'non'].includes(lowerValue)) {
|
||||
return 'Non-Pemerintah';
|
||||
}
|
||||
|
||||
return value; // Return original if no match
|
||||
}
|
||||
|
||||
// Function to insert data into database
|
||||
async function insertDataToDatabase(data: any[]) {
|
||||
let imported = 0;
|
||||
let errorCount = 0;
|
||||
const errorMessages: string[] = [];
|
||||
|
||||
console.log('=== DEBUG: Starting beasiswa 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 beasiswa data processing ===');
|
||||
for (const item of data) {
|
||||
try {
|
||||
console.log(`\n--- Processing beasiswa 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 beasiswa check/insert`);
|
||||
|
||||
// Check if beasiswa already exists for this mahasiswa and nama_beasiswa
|
||||
console.log(`Checking existing beasiswa for mahasiswa ID: ${nimValidation.id_mahasiswa}, nama_beasiswa: ${item.nama_beasiswa}`);
|
||||
const { data: existingBeasiswa, error: beasiswaCheckError } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.select('id_beasiswa')
|
||||
.eq('id_mahasiswa', nimValidation.id_mahasiswa)
|
||||
.eq('nama_beasiswa', item.nama_beasiswa)
|
||||
.single();
|
||||
|
||||
if (beasiswaCheckError && beasiswaCheckError.code !== 'PGRST116') {
|
||||
console.log(`❌ Error checking existing beasiswa:`, beasiswaCheckError);
|
||||
}
|
||||
|
||||
if (existingBeasiswa) {
|
||||
console.log(`📝 Updating existing beasiswa (ID: ${existingBeasiswa.id_beasiswa})`);
|
||||
// Update existing beasiswa
|
||||
const { error: updateError } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.update({
|
||||
sumber_beasiswa: item.sumber_beasiswa,
|
||||
beasiswa_status: item.beasiswa_status,
|
||||
jenis_beasiswa: item.jenis_beasiswa
|
||||
})
|
||||
.eq('id_beasiswa', existingBeasiswa.id_beasiswa);
|
||||
|
||||
if (updateError) {
|
||||
errorCount++;
|
||||
const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal memperbarui beasiswa: ${updateError.message}`;
|
||||
console.log(`❌ Update failed: ${errorMsg}`);
|
||||
errorMessages.push(errorMsg);
|
||||
continue;
|
||||
} else {
|
||||
console.log(`✅ Beasiswa updated successfully`);
|
||||
}
|
||||
} else {
|
||||
console.log(`📝 Inserting new beasiswa for mahasiswa ID: ${nimValidation.id_mahasiswa}`);
|
||||
// Insert new beasiswa
|
||||
const { error: insertError } = await supabase
|
||||
.from('beasiswa_mahasiswa')
|
||||
.insert({
|
||||
id_mahasiswa: nimValidation.id_mahasiswa,
|
||||
nama_beasiswa: item.nama_beasiswa,
|
||||
sumber_beasiswa: item.sumber_beasiswa,
|
||||
beasiswa_status: item.beasiswa_status,
|
||||
jenis_beasiswa: item.jenis_beasiswa
|
||||
});
|
||||
|
||||
if (insertError) {
|
||||
errorCount++;
|
||||
const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal menyimpan beasiswa: ${insertError.message}`;
|
||||
console.log(`❌ Insert failed: ${errorMsg}`);
|
||||
errorMessages.push(errorMsg);
|
||||
continue;
|
||||
} else {
|
||||
console.log(`✅ Beasiswa 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 };
|
||||
}
|
||||
146
app/api/keloladata/data-kelompok-keahlian/route.ts
Normal file
146
app/api/keloladata/data-kelompok-keahlian/route.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import supabase from "@/lib/db";
|
||||
|
||||
// GET - Fetch all kelompok keahlian
|
||||
export async function GET() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('kelompok_keahlian')
|
||||
.select('id_kk, nama_kelompok')
|
||||
.order('nama_kelompok');
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching kelompok keahlian:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch kelompok keahlian data" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create new kelompok keahlian
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { nama_kelompok } = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!nama_kelompok) {
|
||||
return NextResponse.json(
|
||||
{ error: "nama_kelompok is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if nama_kelompok already exists
|
||||
const { data: existingKelompok, error: checkError } = await supabase
|
||||
.from('kelompok_keahlian')
|
||||
.select('id_kk')
|
||||
.eq('nama_kelompok', nama_kelompok)
|
||||
.single();
|
||||
|
||||
if (checkError && checkError.code !== 'PGRST116') {
|
||||
throw checkError;
|
||||
}
|
||||
|
||||
if (existingKelompok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Nama kelompok keahlian already exists" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('kelompok_keahlian')
|
||||
.insert([{ nama_kelompok }])
|
||||
.select('id_kk, nama_kelompok')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return NextResponse.json(data, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating kelompok keahlian:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create kelompok keahlian" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update kelompok keahlian
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const { id_kk, nama_kelompok } = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!id_kk || !nama_kelompok) {
|
||||
return NextResponse.json(
|
||||
{ error: "id_kk and nama_kelompok are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if kelompok keahlian exists
|
||||
const { data: existingKelompok, error: checkError } = await supabase
|
||||
.from('kelompok_keahlian')
|
||||
.select('id_kk')
|
||||
.eq('id_kk', id_kk)
|
||||
.single();
|
||||
|
||||
if (checkError && checkError.code !== 'PGRST116') {
|
||||
throw checkError;
|
||||
}
|
||||
|
||||
if (!existingKelompok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Kelompok keahlian not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if nama_kelompok already exists for another kelompok
|
||||
const { data: duplicateNama, error: duplicateError } = await supabase
|
||||
.from('kelompok_keahlian')
|
||||
.select('id_kk')
|
||||
.eq('nama_kelompok', nama_kelompok)
|
||||
.neq('id_kk', id_kk)
|
||||
.single();
|
||||
|
||||
if (duplicateError && duplicateError.code !== 'PGRST116') {
|
||||
throw duplicateError;
|
||||
}
|
||||
|
||||
if (duplicateNama) {
|
||||
return NextResponse.json(
|
||||
{ error: "Nama kelompok keahlian already exists for another kelompok" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('kelompok_keahlian')
|
||||
.update({ nama_kelompok })
|
||||
.eq('id_kk', id_kk)
|
||||
.select('id_kk, nama_kelompok')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error("Error updating kelompok keahlian:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update kelompok keahlian" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
241
app/api/keloladata/data-mahasiswa/route.ts
Normal file
241
app/api/keloladata/data-mahasiswa/route.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
// GET - Fetch all mahasiswa or a specific one by NIM
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const nim = searchParams.get('nim');
|
||||
|
||||
if (nim) {
|
||||
// Fetch specific mahasiswa by NIM with joins
|
||||
const { data, error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select(`
|
||||
*,
|
||||
kelompok_keahlian!id_kelompok_keahlian(id_kk, nama_kelompok)
|
||||
`)
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return NextResponse.json({ message: 'Mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the joined fields
|
||||
const transformedData = {
|
||||
...data,
|
||||
nama_kelompok_keahlian: data.kelompok_keahlian?.nama_kelompok || null
|
||||
};
|
||||
delete transformedData.kelompok_keahlian;
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
} else {
|
||||
// Fetch all mahasiswa with joins
|
||||
const { data, error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select(`
|
||||
*,
|
||||
kelompok_keahlian!id_kelompok_keahlian(id_kk, nama_kelompok)
|
||||
`)
|
||||
.order('nim');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the joined fields
|
||||
const transformedData = data.map(item => ({
|
||||
...item,
|
||||
nama_kelompok_keahlian: item.kelompok_keahlian?.nama_kelompok || null
|
||||
})).map(({ kelompok_keahlian, ...rest }) => rest);
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create a new mahasiswa
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
nim,
|
||||
nama,
|
||||
jk,
|
||||
agama,
|
||||
kabupaten,
|
||||
provinsi,
|
||||
jenis_pendaftaran,
|
||||
tahun_angkatan,
|
||||
ipk,
|
||||
id_kelompok_keahlian,
|
||||
status_kuliah,
|
||||
semester
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!nim || !nama || !jk || !tahun_angkatan) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Missing required fields: nim, nama, jk, tahun_angkatan' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if mahasiswa already exists
|
||||
const { data: existing, error: checkError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('nim')
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (existing) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Mahasiswa with this NIM already exists' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Insert new mahasiswa
|
||||
const { data, error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.insert({
|
||||
nim,
|
||||
nama,
|
||||
jk,
|
||||
agama: agama || null,
|
||||
kabupaten: kabupaten || null,
|
||||
provinsi: provinsi || null,
|
||||
jenis_pendaftaran: jenis_pendaftaran || null,
|
||||
tahun_angkatan,
|
||||
ipk: ipk || null,
|
||||
id_kelompok_keahlian: id_kelompok_keahlian || null,
|
||||
status_kuliah: status_kuliah || "Aktif",
|
||||
semester: semester || 1
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Mahasiswa created successfully', nim },
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error creating mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update an existing mahasiswa
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const nim = searchParams.get('nim');
|
||||
|
||||
if (!nim) {
|
||||
return NextResponse.json({ message: 'NIM is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
nama,
|
||||
jk,
|
||||
agama,
|
||||
kabupaten,
|
||||
provinsi,
|
||||
jenis_pendaftaran,
|
||||
tahun_angkatan,
|
||||
ipk,
|
||||
id_kelompok_keahlian,
|
||||
status_kuliah,
|
||||
semester
|
||||
} = body;
|
||||
|
||||
// Check if mahasiswa exists
|
||||
const { data: existing, error: checkError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('*')
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (checkError || !existing) {
|
||||
return NextResponse.json({ message: 'Mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Update mahasiswa
|
||||
const { error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.update({
|
||||
nama: nama || existing.nama,
|
||||
jk: jk || existing.jk,
|
||||
agama: agama || existing.agama,
|
||||
kabupaten: kabupaten || existing.kabupaten,
|
||||
provinsi: provinsi || existing.provinsi,
|
||||
jenis_pendaftaran: jenis_pendaftaran || existing.jenis_pendaftaran,
|
||||
tahun_angkatan: tahun_angkatan || existing.tahun_angkatan,
|
||||
ipk: ipk || existing.ipk,
|
||||
id_kelompok_keahlian: id_kelompok_keahlian || existing.id_kelompok_keahlian,
|
||||
status_kuliah: status_kuliah || existing.status_kuliah,
|
||||
semester: semester || existing.semester
|
||||
})
|
||||
.eq('nim', nim);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Mahasiswa updated successfully', nim });
|
||||
} catch (error) {
|
||||
console.error('Error updating mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Delete a mahasiswa
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const nim = searchParams.get('nim');
|
||||
|
||||
if (!nim) {
|
||||
return NextResponse.json({ message: 'NIM is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if mahasiswa exists
|
||||
const { data: existing, error: checkError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('nim')
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (checkError || !existing) {
|
||||
return NextResponse.json({ message: 'Mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete mahasiswa
|
||||
const { error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.delete()
|
||||
.eq('nim', nim);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Mahasiswa deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
360
app/api/keloladata/data-mahasiswa/upload/route.ts
Normal file
360
app/api/keloladata/data-mahasiswa/upload/route.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Get form data from request
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ message: 'No file uploaded' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Read file content as array buffer
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
// Process file data based on file type
|
||||
let validData = [];
|
||||
let errors: string[] = [];
|
||||
|
||||
if (file.name.endsWith('.csv') || file.type === 'text/csv') {
|
||||
// Process as CSV
|
||||
const fileContent = await file.text();
|
||||
const result = await processCSVData(fileContent);
|
||||
validData = result.validData;
|
||||
errors = result.errors;
|
||||
} else {
|
||||
// Process as Excel
|
||||
const result = await processExcelData(fileBuffer);
|
||||
validData = result.validData;
|
||||
errors = result.errors;
|
||||
}
|
||||
|
||||
if (validData.length === 0) {
|
||||
return NextResponse.json({
|
||||
message: 'No valid data found in the file',
|
||||
errors
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Insert valid data into the database
|
||||
const { insertedCount, errorCount } = await insertDataToDatabase(validData);
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'File processed successfully',
|
||||
insertedCount,
|
||||
errorCount,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing file upload:', error);
|
||||
return NextResponse.json({
|
||||
message: 'Error processing file upload',
|
||||
error: (error as Error).message
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process Excel data
|
||||
async function processExcelData(fileBuffer: ArrayBuffer) {
|
||||
try {
|
||||
// Parse Excel file
|
||||
const workbook = XLSX.read(fileBuffer, { type: 'array' });
|
||||
|
||||
// Get first sheet
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
|
||||
// Convert to JSON with proper typing
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
||||
|
||||
if (jsonData.length === 0) {
|
||||
return { validData: [], errors: ['Excel file is empty'] };
|
||||
}
|
||||
|
||||
// Convert Excel data to CSV-like format for processing
|
||||
const headers = jsonData[0].map(h => String(h).toLowerCase());
|
||||
const rows = jsonData.slice(1);
|
||||
|
||||
// Process the data using the common function
|
||||
return processData(headers, rows);
|
||||
} catch (error) {
|
||||
console.error('Error processing Excel data:', error);
|
||||
return { validData: [], errors: [(error as Error).message] };
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process CSV data
|
||||
async function processCSVData(fileContent: string) {
|
||||
const lines = fileContent.split(/\r?\n/).filter(line => line.trim() !== '');
|
||||
|
||||
if (lines.length === 0) {
|
||||
return { validData: [], errors: ['CSV file is empty'] };
|
||||
}
|
||||
|
||||
// Get headers from first line
|
||||
const headerLine = lines[0].toLowerCase();
|
||||
const headers = headerLine.split(',').map(h => h.trim());
|
||||
|
||||
// Process data rows
|
||||
const rows = lines.slice(1).map(line => line.split(',').map(v => v.trim()));
|
||||
|
||||
return processData(headers, rows);
|
||||
}
|
||||
|
||||
// Common function to process data regardless of source format
|
||||
function processData(headers: string[], rows: any[][]) {
|
||||
// Define expected headers and their possible variations
|
||||
const expectedHeaderMap = {
|
||||
nim: ['nim'],
|
||||
nama: ['nama', 'name'],
|
||||
jenis_kelamin: ['jenis_kelamin', 'jk', 'gender'],
|
||||
agama: ['agama', 'religion'],
|
||||
kabupaten: ['kabupaten', 'kota', 'city'],
|
||||
provinsi: ['provinsi', 'province'],
|
||||
jenis_pendaftaran: ['jenis_pendaftaran', 'jalur_masuk', 'admission_type'],
|
||||
tahun_angkatan: ['tahun_angkatan', 'angkatan', 'tahun', 'year'],
|
||||
ipk: ['ipk', 'gpa'],
|
||||
kelompok_keahlian: ['kelompok_keahlian', 'kk', 'keahlian', 'id_kk'],
|
||||
status_kuliah: ['status_kuliah', 'status', 'status_mahasiswa'],
|
||||
semester: ['semester', 'sem']
|
||||
};
|
||||
|
||||
// Map actual headers to expected headers
|
||||
const headerMap: { [key: string]: number } = {};
|
||||
for (const [expectedHeader, variations] of Object.entries(expectedHeaderMap)) {
|
||||
const index = headers.findIndex(h => variations.includes(h));
|
||||
if (index !== -1) {
|
||||
headerMap[expectedHeader] = index;
|
||||
}
|
||||
}
|
||||
|
||||
// Check required headers
|
||||
const requiredHeaders = ['nim', 'nama', 'jenis_kelamin', 'tahun_angkatan'];
|
||||
const missingHeaders = requiredHeaders.filter(h => headerMap[h] === undefined);
|
||||
|
||||
if (missingHeaders.length > 0) {
|
||||
return {
|
||||
validData: [],
|
||||
errors: [`Missing required headers: ${missingHeaders.join(', ')}`]
|
||||
};
|
||||
}
|
||||
|
||||
const validData = [];
|
||||
const errors = [];
|
||||
|
||||
// Process data rows
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const values = rows[i];
|
||||
if (!values || values.length === 0) continue;
|
||||
|
||||
try {
|
||||
// Extract values using header map
|
||||
const nim = String(values[headerMap.nim] || '');
|
||||
const nama = String(values[headerMap.nama] || '');
|
||||
const jenis_kelamin = mapGender(String(values[headerMap.jenis_kelamin] || ''));
|
||||
const tahun_angkatan = String(values[headerMap.tahun_angkatan] || '');
|
||||
|
||||
// Optional fields
|
||||
const agama = headerMap.agama !== undefined ? String(values[headerMap.agama] || '') || null : null;
|
||||
const kabupaten = headerMap.kabupaten !== undefined ? String(values[headerMap.kabupaten] || '') || null : null;
|
||||
const provinsi = headerMap.provinsi !== undefined ? String(values[headerMap.provinsi] || '') || null : null;
|
||||
const jenis_pendaftaran = headerMap.jenis_pendaftaran !== undefined ? String(values[headerMap.jenis_pendaftaran] || '') || null : null;
|
||||
|
||||
// Handle IPK (could be number or string)
|
||||
let ipk = null;
|
||||
if (headerMap.ipk !== undefined && values[headerMap.ipk] !== undefined && values[headerMap.ipk] !== null) {
|
||||
const ipkValue = values[headerMap.ipk];
|
||||
const ipkStr = typeof ipkValue === 'number' ? ipkValue.toString() : String(ipkValue);
|
||||
ipk = ipkStr ? parseFloat(ipkStr) : null;
|
||||
}
|
||||
|
||||
// Handle kelompok_keahlian (could be number or string)
|
||||
let kelompok_keahlian_id = null;
|
||||
if (headerMap.kelompok_keahlian !== undefined && values[headerMap.kelompok_keahlian] !== undefined) {
|
||||
const kkValue = values[headerMap.kelompok_keahlian];
|
||||
if (kkValue !== null && kkValue !== '') {
|
||||
kelompok_keahlian_id = typeof kkValue === 'number' ? kkValue : parseInt(String(kkValue));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle status_kuliah
|
||||
let status_kuliah = 'Aktif'; // Default value
|
||||
if (headerMap.status_kuliah !== undefined && values[headerMap.status_kuliah] !== undefined) {
|
||||
const statusValue = String(values[headerMap.status_kuliah] || '').trim();
|
||||
if (statusValue) {
|
||||
const mappedStatus = mapStatus(statusValue);
|
||||
if (mappedStatus) {
|
||||
status_kuliah = mappedStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle semester (could be number or string)
|
||||
let semester = 1; // Default value
|
||||
if (headerMap.semester !== undefined && values[headerMap.semester] !== undefined) {
|
||||
const semesterValue = values[headerMap.semester];
|
||||
if (semesterValue !== null && semesterValue !== '') {
|
||||
const semesterNum = typeof semesterValue === 'number' ? semesterValue : parseInt(String(semesterValue));
|
||||
if (!isNaN(semesterNum) && semesterNum >= 1 && semesterNum <= 14) {
|
||||
semester = semesterNum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!nim || !nama || !jenis_kelamin || !tahun_angkatan) {
|
||||
errors.push(`Row ${i+1}: Missing required fields`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate NIM format (should be alphanumeric and proper length)
|
||||
if (!/^[A-Za-z0-9]{8,12}$/.test(nim)) {
|
||||
errors.push(`Row ${i+1}: Invalid NIM format - ${nim}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate tahun_angkatan format (should be 4 digits)
|
||||
if (!/^\d{4}$/.test(tahun_angkatan)) {
|
||||
errors.push(`Row ${i+1}: Invalid tahun_angkatan format - ${tahun_angkatan}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate IPK if provided
|
||||
if (ipk !== null && (isNaN(ipk) || ipk < 0 || ipk > 4)) {
|
||||
errors.push(`Row ${i+1}: Invalid IPK value - ${ipk}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate semester if provided
|
||||
if (semester < 1 || semester > 14) {
|
||||
errors.push(`Row ${i+1}: Invalid semester value - ${semester} (must be between 1-14)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to valid data
|
||||
validData.push({
|
||||
nim,
|
||||
nama,
|
||||
jk: jenis_kelamin,
|
||||
agama,
|
||||
kabupaten,
|
||||
provinsi,
|
||||
jenis_pendaftaran,
|
||||
tahun_angkatan,
|
||||
ipk,
|
||||
kelompok_keahlian_id,
|
||||
status_kuliah,
|
||||
semester
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
errors.push(`Row ${i+1}: Error processing row - ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { validData, errors };
|
||||
}
|
||||
|
||||
// Function to map gender values to standardized format
|
||||
function mapGender(value: string): 'Pria' | 'Wanita' | null {
|
||||
if (!value) return null;
|
||||
|
||||
const lowerValue = value.toLowerCase();
|
||||
|
||||
if (['pria', 'laki-laki', 'laki', 'l', 'male', 'm', 'p'].includes(lowerValue)) {
|
||||
return 'Pria';
|
||||
}
|
||||
|
||||
if (['wanita', 'perempuan', 'w', 'female', 'f', 'woman', 'w'].includes(lowerValue)) {
|
||||
return 'Wanita';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function to map status values to standardized format
|
||||
function mapStatus(value: string): 'Aktif' | 'Cuti' | 'Lulus' | 'Non-Aktif' | null {
|
||||
if (!value) return null;
|
||||
|
||||
const lowerValue = value.toLowerCase();
|
||||
|
||||
if (['aktif', 'active', 'a'].includes(lowerValue)) {
|
||||
return 'Aktif';
|
||||
}
|
||||
|
||||
if (['cuti', 'leave', 'c'].includes(lowerValue)) {
|
||||
return 'Cuti';
|
||||
}
|
||||
|
||||
if (['lulus', 'graduated', 'graduate', 'l'].includes(lowerValue)) {
|
||||
return 'Lulus';
|
||||
}
|
||||
|
||||
if (['non-aktif', 'non aktif', 'nonaktif', 'non-aktif', 'non aktif', 'nonaktif', 'n'].includes(lowerValue)) {
|
||||
return 'Non-Aktif';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function to insert data into database
|
||||
async function insertDataToDatabase(data: any[]) {
|
||||
let insertedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const item of data) {
|
||||
try {
|
||||
// Check if mahasiswa already exists
|
||||
const { data: existingData } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('nim')
|
||||
.eq('nim', item.nim)
|
||||
.single();
|
||||
|
||||
const mahasiswaData = {
|
||||
nama: item.nama,
|
||||
jk: item.jk,
|
||||
agama: item.agama,
|
||||
kabupaten: item.kabupaten,
|
||||
provinsi: item.provinsi,
|
||||
jenis_pendaftaran: item.jenis_pendaftaran,
|
||||
tahun_angkatan: item.tahun_angkatan,
|
||||
ipk: item.ipk,
|
||||
id_kelompok_keahlian: item.kelompok_keahlian_id,
|
||||
status_kuliah: item.status_kuliah,
|
||||
semester: item.semester
|
||||
};
|
||||
|
||||
if (existingData) {
|
||||
// Update existing record
|
||||
const { error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.update(mahasiswaData)
|
||||
.eq('nim', item.nim);
|
||||
|
||||
if (error) throw error;
|
||||
} else {
|
||||
// Insert new record
|
||||
const { error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.insert({
|
||||
nim: item.nim,
|
||||
...mahasiswaData
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
insertedCount++;
|
||||
} catch (error) {
|
||||
console.error(`Error inserting/updating record for NIM ${item.nim}:`, error);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return { insertedCount, errorCount };
|
||||
}
|
||||
336
app/api/keloladata/data-prestasi-mahasiswa/route.ts
Normal file
336
app/api/keloladata/data-prestasi-mahasiswa/route.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
// GET - Fetch all prestasi mahasiswa or filter by criteria
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
const id_mahasiswa = searchParams.get('id_mahasiswa');
|
||||
const search = searchParams.get('search');
|
||||
const jenisPrestasi = searchParams.get('jenis_prestasi');
|
||||
const tingkat = searchParams.get('tingkat');
|
||||
|
||||
// If ID is provided, fetch specific prestasi by ID with join
|
||||
if (id) {
|
||||
const { data, error } = await supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.select(`
|
||||
*,
|
||||
mahasiswa!inner(nama, nim)
|
||||
`)
|
||||
.eq('id_prestasi', id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return NextResponse.json({ message: 'Prestasi mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the nama and nim fields
|
||||
const transformedData = {
|
||||
...data,
|
||||
nama: data.mahasiswa.nama,
|
||||
nim: data.mahasiswa.nim
|
||||
};
|
||||
delete transformedData.mahasiswa;
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
}
|
||||
|
||||
// If id_mahasiswa is provided, fetch prestasi for specific student with join
|
||||
if (id_mahasiswa) {
|
||||
const { data, error } = await supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.select(`
|
||||
*,
|
||||
mahasiswa!inner(nama, nim)
|
||||
`)
|
||||
.eq('id_mahasiswa', id_mahasiswa)
|
||||
.order('tanggal_prestasi', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the nama and nim fields
|
||||
const transformedData = data.map(item => ({
|
||||
...item,
|
||||
nama: item.mahasiswa.nama,
|
||||
nim: item.mahasiswa.nim
|
||||
})).map(({ mahasiswa, ...rest }) => rest);
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
}
|
||||
|
||||
// Build the query based on filters with join
|
||||
let query = supabase.from('prestasi_mahasiswa').select(`
|
||||
*,
|
||||
mahasiswa!inner(nama, nim)
|
||||
`);
|
||||
|
||||
// Add search condition if provided
|
||||
if (search) {
|
||||
query = query.or(`mahasiswa.nama.ilike.%${search}%,mahasiswa.nim.ilike.%${search}%,nama_prestasi.ilike.%${search}%,peringkat.ilike.%${search}%,keterangan.ilike.%${search}%`);
|
||||
}
|
||||
|
||||
// Add jenis_prestasi filter if provided
|
||||
if (jenisPrestasi) {
|
||||
query = query.eq('jenis_prestasi', jenisPrestasi);
|
||||
}
|
||||
|
||||
// Add tingkat filter if provided
|
||||
if (tingkat) {
|
||||
query = query.eq('tingkat_prestasi', tingkat);
|
||||
}
|
||||
|
||||
// Add order by
|
||||
query = query.order('tanggal_prestasi', { ascending: false });
|
||||
|
||||
// Execute the query
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Transform the data to flatten the nama and nim fields
|
||||
const transformedData = data.map(item => ({
|
||||
...item,
|
||||
nama: item.mahasiswa.nama,
|
||||
nim: item.mahasiswa.nim
|
||||
})).map(({ mahasiswa, ...rest }) => rest);
|
||||
|
||||
return NextResponse.json(transformedData);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create a new prestasi mahasiswa
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
nim,
|
||||
jenis_prestasi,
|
||||
nama_prestasi,
|
||||
tingkat_prestasi,
|
||||
peringkat,
|
||||
tanggal_prestasi,
|
||||
keterangan
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!nim || !jenis_prestasi || !nama_prestasi || !tingkat_prestasi || !peringkat || !tanggal_prestasi) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Missing required fields: nim, jenis_prestasi, nama_prestasi, tingkat_prestasi, peringkat, tanggal_prestasi' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if mahasiswa exists by NIM and get id_mahasiswa
|
||||
const { data: mahasiswaExists, error: checkError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('id_mahasiswa, nama')
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (checkError || !mahasiswaExists) {
|
||||
return NextResponse.json(
|
||||
{ message: `Mahasiswa dengan NIM ${nim} tidak terdaftar` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate enum values
|
||||
const validJenisPrestasi = ['Akademik', 'Non-Akademik'];
|
||||
const validTingkat = ['Kabupaten', 'Provinsi', 'Nasional', 'Internasional'];
|
||||
|
||||
if (!validJenisPrestasi.includes(jenis_prestasi)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid jenis_prestasi value. Must be one of: Akademik, Non-Akademik' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!validTingkat.includes(tingkat_prestasi)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid tingkat_prestasi value. Must be one of: Kabupaten, Provinsi, Nasional, Internasional' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Insert new prestasi using id_mahasiswa
|
||||
const { data, error } = await supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.insert({
|
||||
id_mahasiswa: mahasiswaExists.id_mahasiswa,
|
||||
jenis_prestasi,
|
||||
nama_prestasi,
|
||||
tingkat_prestasi,
|
||||
peringkat,
|
||||
tanggal_prestasi,
|
||||
keterangan: keterangan || null
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating prestasi mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: `Prestasi berhasil ditambahkan`,
|
||||
id: data.id_prestasi
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error creating prestasi mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update an existing prestasi mahasiswa
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
nim,
|
||||
jenis_prestasi,
|
||||
nama_prestasi,
|
||||
tingkat_prestasi,
|
||||
peringkat,
|
||||
tanggal_prestasi,
|
||||
keterangan
|
||||
} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!nim || !jenis_prestasi || !nama_prestasi || !tingkat_prestasi || !peringkat || !tanggal_prestasi) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Missing required fields: nim, jenis_prestasi, nama_prestasi, tingkat_prestasi, peringkat, tanggal_prestasi' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if prestasi exists
|
||||
const { data: existing, error: checkError } = await supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.select('*')
|
||||
.eq('id_prestasi', id)
|
||||
.single();
|
||||
|
||||
if (checkError || !existing) {
|
||||
return NextResponse.json({ message: 'Prestasi mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if mahasiswa exists by NIM and get id_mahasiswa
|
||||
const { data: mahasiswaExists, error: mahasiswaCheckError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('id_mahasiswa, nama')
|
||||
.eq('nim', nim)
|
||||
.single();
|
||||
|
||||
if (mahasiswaCheckError || !mahasiswaExists) {
|
||||
return NextResponse.json(
|
||||
{ message: `Mahasiswa dengan NIM ${nim} tidak terdaftar` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate enum values
|
||||
const validJenisPrestasi = ['Akademik', 'Non-Akademik'];
|
||||
const validTingkat = ['Kabupaten', 'Provinsi', 'Nasional', 'Internasional'];
|
||||
|
||||
if (!validJenisPrestasi.includes(jenis_prestasi)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid jenis_prestasi value. Must be one of: Akademik, Non-Akademik' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!validTingkat.includes(tingkat_prestasi)) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid tingkat_prestasi value. Must be one of: Kabupaten, Provinsi, Nasional, Internasional' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update prestasi using id_mahasiswa
|
||||
const { error } = await supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.update({
|
||||
id_mahasiswa: mahasiswaExists.id_mahasiswa,
|
||||
jenis_prestasi,
|
||||
nama_prestasi,
|
||||
tingkat_prestasi,
|
||||
peringkat,
|
||||
tanggal_prestasi,
|
||||
keterangan: keterangan || null
|
||||
})
|
||||
.eq('id_prestasi', id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating prestasi mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Prestasi berhasil diperbarui`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating prestasi mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Delete a prestasi mahasiswa
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ message: 'ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if prestasi exists
|
||||
const { data: existing, error: checkError } = await supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.select('id_prestasi')
|
||||
.eq('id_prestasi', id)
|
||||
.single();
|
||||
|
||||
if (checkError || !existing) {
|
||||
return NextResponse.json({ message: 'Prestasi mahasiswa not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete prestasi
|
||||
const { error } = await supabase
|
||||
.from('prestasi_mahasiswa')
|
||||
.delete()
|
||||
.eq('id_prestasi', id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting prestasi mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Prestasi mahasiswa deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting prestasi mahasiswa:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
470
app/api/keloladata/data-prestasi-mahasiswa/upload/route.ts
Normal file
470
app/api/keloladata/data-prestasi-mahasiswa/upload/route.ts
Normal file
@@ -0,0 +1,470 @@
|
||||
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 };
|
||||
}
|
||||
63
app/api/keloladata/setting-jenis-pendaftaran/route.ts
Normal file
63
app/api/keloladata/setting-jenis-pendaftaran/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
// GET - Fetch all unique jenis_pendaftaran values
|
||||
export async function GET() {
|
||||
try {
|
||||
// Get all unique jenis_pendaftaran values
|
||||
const { data, error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('jenis_pendaftaran')
|
||||
.not('jenis_pendaftaran', 'is', null)
|
||||
.order('jenis_pendaftaran');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching jenis pendaftaran data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Get unique values
|
||||
const uniqueValues = [...new Set(data.map(item => item.jenis_pendaftaran))];
|
||||
|
||||
return NextResponse.json(uniqueValues.map(value => ({ jenis_pendaftaran: value })));
|
||||
} catch (error) {
|
||||
console.error('Error fetching jenis pendaftaran data:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update jenis_pendaftaran value
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { oldValue, newValue } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!oldValue || !newValue) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Missing required fields: oldValue, newValue' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update jenis_pendaftaran
|
||||
const { data, error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.update({ jenis_pendaftaran: newValue })
|
||||
.eq('jenis_pendaftaran', oldValue)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating jenis pendaftaran:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Jenis pendaftaran updated successfully',
|
||||
affectedRows: data?.length || 0
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating jenis pendaftaran:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
94
app/api/keloladata/update-semester/route.ts
Normal file
94
app/api/keloladata/update-semester/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import supabase from '@/lib/db';
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
// Get current date
|
||||
const currentDate = new Date();
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11
|
||||
|
||||
// Get all active students
|
||||
const { data: activeStudents, error: fetchError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('nim, tahun_angkatan, semester')
|
||||
.eq('status_kuliah', 'Aktif');
|
||||
|
||||
if (fetchError) {
|
||||
console.error('Error fetching active students:', fetchError);
|
||||
return NextResponse.json(
|
||||
{ message: "Gagal mengambil data mahasiswa aktif" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeStudents || activeStudents.length === 0) {
|
||||
return NextResponse.json({
|
||||
message: "Tidak ada mahasiswa aktif yang ditemukan",
|
||||
affectedRows: 0
|
||||
});
|
||||
}
|
||||
|
||||
let updatedCount = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
// Update semester for each active student
|
||||
for (const student of activeStudents) {
|
||||
try {
|
||||
const tahunAngkatan = student.tahun_angkatan;
|
||||
if (!tahunAngkatan) {
|
||||
errors.push(`Mahasiswa NIM ${student.nim}: Tahun angkatan tidak ditemukan`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate current semester based on tahun_angkatan and current date
|
||||
const yearsSinceEnrollment = currentYear - tahunAngkatan;
|
||||
let currentSemester = yearsSinceEnrollment * 2; // 2 semesters per year
|
||||
|
||||
// Adjust for current month (odd months = odd semesters, even months = even semesters)
|
||||
if (currentMonth >= 2 && currentMonth <= 7) {
|
||||
// February to July = odd semester (1, 3, 5, etc.)
|
||||
currentSemester += 1;
|
||||
} else {
|
||||
// August to January = even semester (2, 4, 6, etc.)
|
||||
currentSemester += 2;
|
||||
}
|
||||
|
||||
// Cap at semester 14 (7 years)
|
||||
if (currentSemester > 14) {
|
||||
currentSemester = 14;
|
||||
}
|
||||
|
||||
// Update semester if different
|
||||
if (student.semester !== currentSemester) {
|
||||
const { error: updateError } = await supabase
|
||||
.from('mahasiswa')
|
||||
.update({ semester: currentSemester })
|
||||
.eq('nim', student.nim);
|
||||
|
||||
if (updateError) {
|
||||
errors.push(`Mahasiswa NIM ${student.nim}: Gagal memperbarui semester: ${updateError.message}`);
|
||||
} else {
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating semester for mahasiswa NIM ${student.nim}:`, error);
|
||||
errors.push(`Mahasiswa NIM ${student.nim}: Terjadi kesalahan: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Berhasil memperbarui semester untuk ${updatedCount} mahasiswa`,
|
||||
affectedRows: updatedCount,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in update semester:', error);
|
||||
return NextResponse.json(
|
||||
{ message: "Terjadi kesalahan internal server" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export async function GET(request: Request) {
|
||||
let query = supabase
|
||||
.from('mahasiswa')
|
||||
.select('tahun_angkatan, status_kuliah')
|
||||
.in('status_kuliah', ['Lulus', 'Cuti', 'Aktif', 'DO']);
|
||||
.in('status_kuliah', ['Lulus', 'Cuti', 'Aktif', 'Non-Aktif']);
|
||||
|
||||
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||
query = query.eq('tahun_angkatan', tahunAngkatan);
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function GET() {
|
||||
const { data, error } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('status_kuliah, tahun_angkatan')
|
||||
.in('status_kuliah', ['Lulus', 'Cuti', 'Aktif', 'DO']);
|
||||
.in('status_kuliah', ['Lulus', 'Cuti', 'Aktif', 'Non-Aktif']);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching status data:', error);
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function OPTIONS() {
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Get total mahasiswa
|
||||
// jumlah mahasiswa
|
||||
const { count: totalMahasiswa } = await supabase
|
||||
.from('mahasiswa')
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
Reference in New Issue
Block a user