From c77321bc8a5a1eaaee1d0e2f4b33ff1322597ddd Mon Sep 17 00:00:00 2001 From: Randa Firman Putra Date: Thu, 6 Nov 2025 18:31:44 +0700 Subject: [PATCH] again n again --- app/api/dosen/list/route.ts | 40 + app/api/keloladata/data-mata-kuliah/route.ts | 393 +++++++++ .../keloladata/data-nilai-mahasiswa/route.ts | 420 +++++++++ .../data-nilai-mahasiswa/upload/route.ts | 311 +++++++ .../keloladata/upload-mata-kuliah/route.ts | 257 ++++++ app/api/tabeldetail/asal-daerah/route.ts | 65 ++ app/api/tabeldetail/asal-provinsi/route.ts | 65 ++ app/api/tabeldetail/bimbingan-dosen/route.ts | 82 ++ app/api/tabeldetail/nama-beasiswa/route.ts | 69 ++ app/api/tabeldetail/tingkat-prestasi/route.ts | 72 ++ app/detail/asal-daerah/page.tsx | 5 + app/detail/asal-provinsi/page.tsx | 16 + app/detail/bimbingan-dosen/page.tsx | 18 + app/detail/nama-beasiswa/page.tsx | 29 +- app/detail/tingkat-prestasi/page.tsx | 10 +- app/keloladata/matakuliah/page.tsx | 9 + app/keloladata/nilaimahasiswa/page.tsx | 9 + components/charts/AsalDaerahChart.tsx | 31 +- components/charts/BimbinganDosenChart.tsx | 70 +- .../charts/BimbinganDosenPerAngkatanChart.tsx | 70 +- .../chartsDashboard/NamaBeasiswaDashChart.tsx | 323 ++++--- .../TingkatPrestasiDashChart.tsx | 105 ++- .../chartsDashboard/kkdashboardchart.tsx | 85 +- .../chartstable/tabelasaldaerahmahasiswa.tsx | 378 ++++++++ .../tabelasalprovinsi mahasiswa.tsx | 372 ++++++++ .../tabelbimbingandosenmahasiswa.tsx | 417 +++++++++ .../tabelnamabeasiswamahasiswa.tsx | 372 ++++++++ .../tabeltingkatprestasimahasiswa.tsx | 397 +++++++++ .../datatable/data-table-mata-kuliah.tsx | 787 +++++++++++++++++ .../datatable/data-table-nilai-mahasiswa.tsx | 835 ++++++++++++++++++ .../datatable/upload-file-mata-kuliah.tsx | 187 ++++ .../datatable/upload-file-nilai-mahasiswa.tsx | 167 ++++ components/ui/Navbar.tsx | 22 +- components/ui/filter-nama-dosen.tsx | 52 ++ package-lock.json | 7 + package.json | 1 + 36 files changed, 6363 insertions(+), 185 deletions(-) create mode 100644 app/api/dosen/list/route.ts create mode 100644 app/api/keloladata/data-mata-kuliah/route.ts create mode 100644 app/api/keloladata/data-nilai-mahasiswa/route.ts create mode 100644 app/api/keloladata/data-nilai-mahasiswa/upload/route.ts create mode 100644 app/api/keloladata/upload-mata-kuliah/route.ts create mode 100644 app/api/tabeldetail/asal-daerah/route.ts create mode 100644 app/api/tabeldetail/asal-provinsi/route.ts create mode 100644 app/api/tabeldetail/bimbingan-dosen/route.ts create mode 100644 app/api/tabeldetail/nama-beasiswa/route.ts create mode 100644 app/api/tabeldetail/tingkat-prestasi/route.ts create mode 100644 app/keloladata/matakuliah/page.tsx create mode 100644 app/keloladata/nilaimahasiswa/page.tsx create mode 100644 components/chartstable/tabelasaldaerahmahasiswa.tsx create mode 100644 components/chartstable/tabelasalprovinsi mahasiswa.tsx create mode 100644 components/chartstable/tabelbimbingandosenmahasiswa.tsx create mode 100644 components/chartstable/tabelnamabeasiswamahasiswa.tsx create mode 100644 components/chartstable/tabeltingkatprestasimahasiswa.tsx create mode 100644 components/datatable/data-table-mata-kuliah.tsx create mode 100644 components/datatable/data-table-nilai-mahasiswa.tsx create mode 100644 components/datatable/upload-file-mata-kuliah.tsx create mode 100644 components/datatable/upload-file-nilai-mahasiswa.tsx create mode 100644 components/ui/filter-nama-dosen.tsx diff --git a/app/api/dosen/list/route.ts b/app/api/dosen/list/route.ts new file mode 100644 index 0000000..471bbf2 --- /dev/null +++ b/app/api/dosen/list/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +// GET - Ambil daftar nama dosen yang menjadi pembimbing +export async function GET() { + try { + const { data, error } = await supabase + .from('mahasiswa') + .select(` + dosen_pembimbing_1:dosen!pembimbing_1(nama_dosen), + dosen_pembimbing_2:dosen!pembimbing_2(nama_dosen) + `) + .or('pembimbing_1.not.is.null,pembimbing_2.not.is.null'); + + if (error) { + console.error('Error fetching dosen list:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + // Extract unique dosen names + const uniqueDosen = new Set(); + + data.forEach((item: any) => { + if (item.dosen_pembimbing_1?.nama_dosen) { + uniqueDosen.add(item.dosen_pembimbing_1.nama_dosen); + } + if (item.dosen_pembimbing_2?.nama_dosen) { + uniqueDosen.add(item.dosen_pembimbing_2.nama_dosen); + } + }); + + // Convert to sorted array + const sortedDosen = Array.from(uniqueDosen).sort(); + + return NextResponse.json(sortedDosen); + } catch (error) { + console.error('Error fetching dosen list:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/api/keloladata/data-mata-kuliah/route.ts b/app/api/keloladata/data-mata-kuliah/route.ts new file mode 100644 index 0000000..10700b2 --- /dev/null +++ b/app/api/keloladata/data-mata-kuliah/route.ts @@ -0,0 +1,393 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +// Define the MataKuliah type +interface MataKuliah { + id_mk: number; + kode_mk: string; + nama_mk: string; + sks: number; + semester: number; + jenis_mk: 'Wajib' | 'Pilihan Wajib' | 'Pilihan'; + id_prasyarat: number | null; + nama_prasyarat?: string | null; + created_at: string; + updated_at: string; +} + +// GET - Ambil semua data mata kuliah atau satu mata kuliah spesifik berdasarkan ID +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + const search = searchParams.get('search'); + const semester = searchParams.get('semester'); + + if (id) { + // Ambil mata kuliah spesifik berdasarkan ID dengan join untuk prasyarat + const { data, error } = await supabase + .from('mata_kuliah') + .select(` + id_mk, + kode_mk, + nama_mk, + sks, + semester, + jenis_mk, + id_prasyarat, + created_at, + updated_at, + prasyarat:mata_kuliah!id_prasyarat(kode_mk, nama_mk) + `) + .eq('id_mk', id) + .single(); + + if (error || !data) { + return NextResponse.json({ message: 'Mata kuliah not found' }, { status: 404 }); + } + + // Transformasi data untuk meratakan field yang di-join + const transformedData = { + ...data, + nama_prasyarat: (data.prasyarat as any)?.nama_mk || null + }; + delete (transformedData as any).prasyarat; + + return NextResponse.json(transformedData); + } else { + // Ambil semua mata kuliah terlebih dahulu + let query = supabase + .from('mata_kuliah') + .select('*'); + + // Add search condition if provided + if (search) { + query = query.or(`kode_mk.ilike.%${search}%,nama_mk.ilike.%${search}%`); + } + + // Add semester filter if provided + if (semester && semester !== 'all') { + query = query.eq('semester', parseInt(semester)); + } + + // Add order by + query = query.order('semester', { ascending: true }).order('kode_mk', { ascending: true }); + + const { data: mataKuliahData, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + // Ambil data prasyarat untuk semua mata kuliah yang memiliki prasyarat + const prasyaratIds = mataKuliahData + .filter(mk => mk.id_prasyarat) + .map(mk => mk.id_prasyarat); + + let prasyaratData: any[] = []; + if (prasyaratIds.length > 0) { + const { data: prasyaratResult, error: prasyaratError } = await supabase + .from('mata_kuliah') + .select('id_mk, kode_mk, nama_mk') + .in('id_mk', prasyaratIds); + + if (!prasyaratError) { + prasyaratData = prasyaratResult || []; + } + } + + // Gabungkan data mata kuliah dengan data prasyarat + const transformedData = mataKuliahData.map(item => { + const prasyarat = prasyaratData.find(p => p.id_mk === item.id_prasyarat); + return { + ...item, + nama_prasyarat: prasyarat ? prasyarat.nama_mk : null + }; + }); + + return NextResponse.json(transformedData); + } + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} + +// POST - Buat data mata kuliah baru +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + kode_mk, + nama_mk, + sks, + semester, + jenis_mk, + id_prasyarat + } = body; + + // Validasi field yang wajib diisi + if (!kode_mk || !nama_mk || !sks || !semester || !jenis_mk) { + return NextResponse.json( + { message: 'Missing required fields: kode_mk, nama_mk, sks, semester, jenis_mk' }, + { status: 400 } + ); + } + + // Validasi nilai sks dan semester + if (sks <= 0 || semester <= 0) { + return NextResponse.json( + { message: 'SKS and semester must be greater than 0' }, + { status: 400 } + ); + } + + // Validasi jenis mata kuliah + const validJenisMK = ['Wajib', 'Pilihan Wajib', 'Pilihan']; + if (!validJenisMK.includes(jenis_mk)) { + return NextResponse.json( + { message: 'Invalid jenis_mk value. Must be one of: Wajib, Pilihan Wajib, Pilihan' }, + { status: 400 } + ); + } + + // Cek apakah kode mata kuliah sudah ada + const { data: existing, error: checkError } = await supabase + .from('mata_kuliah') + .select('kode_mk') + .eq('kode_mk', kode_mk) + .single(); + + if (existing) { + return NextResponse.json( + { message: 'Mata kuliah with this code already exists' }, + { status: 409 } + ); + } + + // Validasi prasyarat jika ada + if (id_prasyarat) { + const { data: prasyaratExists, error: prasyaratError } = await supabase + .from('mata_kuliah') + .select('id_mk') + .eq('id_mk', id_prasyarat) + .single(); + + if (prasyaratError || !prasyaratExists) { + return NextResponse.json( + { message: 'Prasyarat mata kuliah not found' }, + { status: 404 } + ); + } + } + + // Insert mata kuliah baru + const { data, error } = await supabase + .from('mata_kuliah') + .insert({ + kode_mk, + nama_mk, + sks: parseInt(sks), + semester: parseInt(semester), + jenis_mk, + id_prasyarat: id_prasyarat || null + }) + .select() + .single(); + + if (error) { + console.error('Error creating mata kuliah:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + return NextResponse.json( + { message: 'Mata kuliah created successfully', id: data.id_mk }, + { status: 201 } + ); + } catch (error) { + console.error('Error creating mata kuliah:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} + +// PUT - Update data mata kuliah yang sudah ada +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 { + kode_mk, + nama_mk, + sks, + semester, + jenis_mk, + id_prasyarat + } = body; + + // Validasi field yang wajib diisi + if (!kode_mk || !nama_mk || !sks || !semester || !jenis_mk) { + return NextResponse.json( + { message: 'Missing required fields: kode_mk, nama_mk, sks, semester, jenis_mk' }, + { status: 400 } + ); + } + + // Validasi nilai sks dan semester + if (sks <= 0 || semester <= 0) { + return NextResponse.json( + { message: 'SKS and semester must be greater than 0' }, + { status: 400 } + ); + } + + // Validasi jenis mata kuliah + const validJenisMK = ['Wajib', 'Pilihan Wajib', 'Pilihan']; + if (!validJenisMK.includes(jenis_mk)) { + return NextResponse.json( + { message: 'Invalid jenis_mk value. Must be one of: Wajib, Pilihan Wajib, Pilihan' }, + { status: 400 } + ); + } + + // Cek apakah mata kuliah ada + const { data: existing, error: checkError } = await supabase + .from('mata_kuliah') + .select('*') + .eq('id_mk', id) + .single(); + + if (checkError || !existing) { + return NextResponse.json({ message: 'Mata kuliah not found' }, { status: 404 }); + } + + // Cek apakah kode mata kuliah sudah digunakan oleh mata kuliah lain + const { data: duplicateCode, error: duplicateError } = await supabase + .from('mata_kuliah') + .select('id_mk') + .eq('kode_mk', kode_mk) + .neq('id_mk', id) + .single(); + + if (duplicateCode) { + return NextResponse.json( + { message: 'Mata kuliah with this code already exists' }, + { status: 409 } + ); + } + + // Validasi prasyarat jika ada + if (id_prasyarat) { + // Pastikan prasyarat tidak sama dengan mata kuliah itu sendiri + if (parseInt(id_prasyarat) === parseInt(id)) { + return NextResponse.json( + { message: 'Mata kuliah cannot be prerequisite of itself' }, + { status: 400 } + ); + } + + const { data: prasyaratExists, error: prasyaratError } = await supabase + .from('mata_kuliah') + .select('id_mk') + .eq('id_mk', id_prasyarat) + .single(); + + if (prasyaratError || !prasyaratExists) { + return NextResponse.json( + { message: 'Prasyarat mata kuliah not found' }, + { status: 404 } + ); + } + } + + // Update mata kuliah + const { error } = await supabase + .from('mata_kuliah') + .update({ + kode_mk, + nama_mk, + sks: parseInt(sks), + semester: parseInt(semester), + jenis_mk, + id_prasyarat: id_prasyarat || null, + updated_at: new Date().toISOString() + }) + .eq('id_mk', id); + + if (error) { + console.error('Error updating mata kuliah:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + return NextResponse.json({ message: 'Mata kuliah updated successfully' }); + } catch (error) { + console.error('Error updating mata kuliah:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} + +// DELETE - Hapus data mata kuliah +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 mata kuliah exists + const { data: existing, error: checkError } = await supabase + .from('mata_kuliah') + .select('id_mk, kode_mk, nama_mk') + .eq('id_mk', id) + .single(); + + if (checkError || !existing) { + return NextResponse.json({ message: 'Mata kuliah not found' }, { status: 404 }); + } + + // Check if mata kuliah is used as prerequisite by other mata kuliah + const { data: dependents, error: dependentsError } = await supabase + .from('mata_kuliah') + .select('kode_mk, nama_mk') + .eq('id_prasyarat', id); + + if (dependentsError) { + console.error('Error checking dependents:', dependentsError); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + if (dependents && dependents.length > 0) { + const dependentNames = dependents.map(d => `${d.kode_mk} - ${d.nama_mk}`).join(', '); + return NextResponse.json( + { + message: `Cannot delete mata kuliah. It is used as prerequisite by: ${dependentNames}` + }, + { status: 400 } + ); + } + + // Hapus mata kuliah + const { error } = await supabase + .from('mata_kuliah') + .delete() + .eq('id_mk', id); + + if (error) { + console.error('Error deleting mata kuliah:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + return NextResponse.json({ message: 'Mata kuliah deleted successfully' }); + } catch (error) { + console.error('Error deleting mata kuliah:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/api/keloladata/data-nilai-mahasiswa/route.ts b/app/api/keloladata/data-nilai-mahasiswa/route.ts new file mode 100644 index 0000000..f97cbd4 --- /dev/null +++ b/app/api/keloladata/data-nilai-mahasiswa/route.ts @@ -0,0 +1,420 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +// Define the NilaiMahasiswa type +interface NilaiMahasiswa { + id_nilai: number; + id_mahasiswa: number; + id_mk: number; + nim: string; + nama: string; + kode_mk: string; + nama_mk: string; + nilai_huruf: 'A' | 'B+' | 'B' | 'C+' | 'C' | 'D+' | 'D' | 'E'; + nilai_angka: number; + semester: number; + created_at: string; + updated_at: string; +} + +// GET - Ambil semua data nilai mahasiswa atau satu nilai spesifik berdasarkan ID +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + const search = searchParams.get('search'); + const semester = searchParams.get('semester'); + const nilai_huruf = searchParams.get('nilai_huruf'); + + if (id) { + // Ambil nilai spesifik berdasarkan ID dengan join ke mahasiswa dan mata_kuliah + const { data, error } = await supabase + .from('nilai_mahasiswa') + .select(` + id_nilai, + id_mahasiswa, + id_mk, + nilai_huruf, + nilai_angka, + semester, + created_at, + updated_at, + mahasiswa!inner(nim, nama), + mata_kuliah!inner(kode_mk, nama_mk) + `) + .eq('id_nilai', id) + .single(); + + if (error || !data) { + return NextResponse.json({ message: 'Nilai mahasiswa not found' }, { status: 404 }); + } + + // Transformasi data untuk meratakan field yang di-join + const transformedData: any = { + ...data, + nim: (data.mahasiswa as any)?.nim || '', + nama: (data.mahasiswa as any)?.nama || '', + kode_mk: (data.mata_kuliah as any)?.kode_mk || '', + nama_mk: (data.mata_kuliah as any)?.nama_mk || '' + }; + delete transformedData.mahasiswa; + delete transformedData.mata_kuliah; + + return NextResponse.json(transformedData); + } else { + // Ambil semua nilai mahasiswa dengan join + let query = supabase + .from('nilai_mahasiswa') + .select(` + id_nilai, + id_mahasiswa, + id_mk, + nilai_huruf, + nilai_angka, + semester, + created_at, + updated_at, + mahasiswa!inner(nim, nama), + mata_kuliah!inner(kode_mk, nama_mk) + `); + + // Add search condition if provided + if (search) { + query = query.or(`mahasiswa.nim.ilike.%${search}%,mahasiswa.nama.ilike.%${search}%,mata_kuliah.kode_mk.ilike.%${search}%,mata_kuliah.nama_mk.ilike.%${search}%`); + } + + // Add semester filter if provided + if (semester && semester !== 'all') { + query = query.eq('semester', parseInt(semester)); + } + + // Add nilai_huruf filter if provided + if (nilai_huruf && nilai_huruf !== 'all') { + query = query.eq('nilai_huruf', nilai_huruf); + } + + // Add order by + query = query.order('semester', { ascending: true }).order('mahasiswa(nim)', { ascending: true }); + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + // Transformasi data untuk meratakan field yang di-join + const transformedData = data.map((item: any) => ({ + ...item, + nim: item.mahasiswa?.nim || '', + nama: item.mahasiswa?.nama || '', + kode_mk: item.mata_kuliah?.kode_mk || '', + nama_mk: item.mata_kuliah?.nama_mk || '' + })).map((item: any) => { + const { mahasiswa, mata_kuliah, ...rest } = item; + return rest; + }); + + return NextResponse.json(transformedData); + } + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} + +// POST - Buat data nilai mahasiswa baru +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + nim, + id_mk, + nilai_huruf, + nilai_angka, + semester + } = body; + + // Validasi field yang wajib diisi + if (!nim || !id_mk || !nilai_huruf || nilai_angka === undefined || !semester) { + return NextResponse.json( + { message: 'Missing required fields: nim, id_mk, nilai_huruf, nilai_angka, semester' }, + { status: 400 } + ); + } + + // Validasi nilai huruf + const validNilaiHuruf = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'E']; + if (!validNilaiHuruf.includes(nilai_huruf)) { + return NextResponse.json( + { message: 'Invalid nilai_huruf. Must be one of: A, B+, B, C+, C, D+, D, E' }, + { status: 400 } + ); + } + + // Validasi nilai angka + if (nilai_angka < 0 || nilai_angka > 4) { + return NextResponse.json( + { message: 'nilai_angka must be between 0 and 4' }, + { status: 400 } + ); + } + + // Validasi semester + if (semester <= 0) { + return NextResponse.json( + { message: 'semester must be greater than 0' }, + { status: 400 } + ); + } + + // Cek apakah mahasiswa dengan NIM tersebut ada + const { data: mahasiswa, error: mahasiswaError } = await supabase + .from('mahasiswa') + .select('id_mahasiswa, nim, nama') + .eq('nim', nim) + .single(); + + if (mahasiswaError || !mahasiswa) { + return NextResponse.json( + { message: `Mahasiswa dengan NIM ${nim} tidak terdaftar` }, + { status: 404 } + ); + } + + // Cek apakah mata kuliah ada + const { data: mataKuliah, error: mkError } = await supabase + .from('mata_kuliah') + .select('id_mk, kode_mk, nama_mk') + .eq('id_mk', id_mk) + .single(); + + if (mkError || !mataKuliah) { + return NextResponse.json( + { message: 'Mata kuliah not found' }, + { status: 404 } + ); + } + + // Cek apakah nilai untuk mahasiswa dan mata kuliah ini sudah ada + const { data: existingNilai, error: checkError } = await supabase + .from('nilai_mahasiswa') + .select('id_nilai') + .eq('id_mahasiswa', mahasiswa.id_mahasiswa) + .eq('id_mk', id_mk) + .single(); + + if (existingNilai) { + return NextResponse.json( + { message: `Nilai untuk mahasiswa ${nim} pada mata kuliah ${mataKuliah.kode_mk} sudah ada` }, + { status: 409 } + ); + } + + // Insert nilai baru + const { data, error } = await supabase + .from('nilai_mahasiswa') + .insert({ + id_mahasiswa: mahasiswa.id_mahasiswa, + id_mk: parseInt(id_mk), + nilai_huruf, + nilai_angka: parseFloat(nilai_angka), + semester: parseInt(semester) + }) + .select() + .single(); + + if (error) { + console.error('Error creating nilai mahasiswa:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + return NextResponse.json( + { + message: 'Nilai mahasiswa berhasil ditambahkan', + id: data.id_nilai, + mahasiswa: mahasiswa.nama, + mata_kuliah: mataKuliah.nama_mk + }, + { status: 201 } + ); + } catch (error) { + console.error('Error creating nilai mahasiswa:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} + +// PUT - Update data nilai mahasiswa yang sudah ada +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, + id_mk, + nilai_huruf, + nilai_angka, + semester + } = body; + + // Validasi field yang wajib diisi + if (!nim || !id_mk || !nilai_huruf || nilai_angka === undefined || !semester) { + return NextResponse.json( + { message: 'Missing required fields: nim, id_mk, nilai_huruf, nilai_angka, semester' }, + { status: 400 } + ); + } + + // Validasi nilai huruf + const validNilaiHuruf = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'E']; + if (!validNilaiHuruf.includes(nilai_huruf)) { + return NextResponse.json( + { message: 'Invalid nilai_huruf. Must be one of: A, B+, B, C+, C, D+, D, E' }, + { status: 400 } + ); + } + + // Validasi nilai angka + if (nilai_angka < 0 || nilai_angka > 4) { + return NextResponse.json( + { message: 'nilai_angka must be between 0 and 4' }, + { status: 400 } + ); + } + + // Validasi semester + if (semester <= 0) { + return NextResponse.json( + { message: 'semester must be greater than 0' }, + { status: 400 } + ); + } + + // Cek apakah nilai ada + const { data: existing, error: checkError } = await supabase + .from('nilai_mahasiswa') + .select('*') + .eq('id_nilai', id) + .single(); + + if (checkError || !existing) { + return NextResponse.json({ message: 'Nilai mahasiswa not found' }, { status: 404 }); + } + + // Cek apakah mahasiswa dengan NIM tersebut ada + const { data: mahasiswa, error: mahasiswaError } = await supabase + .from('mahasiswa') + .select('id_mahasiswa, nim, nama') + .eq('nim', nim) + .single(); + + if (mahasiswaError || !mahasiswa) { + return NextResponse.json( + { message: `Mahasiswa dengan NIM ${nim} tidak terdaftar` }, + { status: 404 } + ); + } + + // Cek apakah mata kuliah ada + const { data: mataKuliah, error: mkError } = await supabase + .from('mata_kuliah') + .select('id_mk, kode_mk, nama_mk') + .eq('id_mk', id_mk) + .single(); + + if (mkError || !mataKuliah) { + return NextResponse.json( + { message: 'Mata kuliah not found' }, + { status: 404 } + ); + } + + // Cek apakah kombinasi mahasiswa-mata kuliah sudah digunakan oleh nilai lain + const { data: duplicateNilai, error: duplicateError } = await supabase + .from('nilai_mahasiswa') + .select('id_nilai') + .eq('id_mahasiswa', mahasiswa.id_mahasiswa) + .eq('id_mk', id_mk) + .neq('id_nilai', id) + .single(); + + if (duplicateNilai) { + return NextResponse.json( + { message: `Nilai untuk mahasiswa ${nim} pada mata kuliah ${mataKuliah.kode_mk} sudah ada` }, + { status: 409 } + ); + } + + // Update nilai + const { error } = await supabase + .from('nilai_mahasiswa') + .update({ + id_mahasiswa: mahasiswa.id_mahasiswa, + id_mk: parseInt(id_mk), + nilai_huruf, + nilai_angka: parseFloat(nilai_angka), + semester: parseInt(semester), + updated_at: new Date().toISOString() + }) + .eq('id_nilai', id); + + if (error) { + console.error('Error updating nilai mahasiswa:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + return NextResponse.json({ message: 'Nilai mahasiswa berhasil diperbarui' }); + } catch (error) { + console.error('Error updating nilai mahasiswa:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} + +// DELETE - Hapus data nilai 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 nilai exists + const { data: existing, error: checkError } = await supabase + .from('nilai_mahasiswa') + .select(` + id_nilai, + mahasiswa!inner(nim, nama), + mata_kuliah!inner(kode_mk, nama_mk) + `) + .eq('id_nilai', id) + .single(); + + if (checkError || !existing) { + return NextResponse.json({ message: 'Nilai mahasiswa not found' }, { status: 404 }); + } + + // Hapus nilai + const { error } = await supabase + .from('nilai_mahasiswa') + .delete() + .eq('id_nilai', id); + + if (error) { + console.error('Error deleting nilai mahasiswa:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } + + return NextResponse.json({ message: 'Nilai mahasiswa berhasil dihapus' }); + } catch (error) { + console.error('Error deleting nilai mahasiswa:', error); + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/api/keloladata/data-nilai-mahasiswa/upload/route.ts b/app/api/keloladata/data-nilai-mahasiswa/upload/route.ts new file mode 100644 index 0000000..231ce38 --- /dev/null +++ b/app/api/keloladata/data-nilai-mahasiswa/upload/route.ts @@ -0,0 +1,311 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; +import * as XLSX from 'xlsx'; + +interface NilaiMahasiswaUpload { + nim: string; + kode_mk: string; + semester: number; + nilai_huruf: 'A' | 'B+' | 'B' | 'C+' | 'C' | 'D+' | 'D' | 'E'; + nilai_angka: number; +} + + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const file = formData.get('file') as File; + + if (!file) { + return NextResponse.json( + { message: 'No file uploaded' }, + { status: 400 } + ); + } + + // Validate file type + const validTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv' + ]; + + if (!validTypes.includes(file.type)) { + return NextResponse.json( + { message: 'Invalid file type. Please upload Excel (.xlsx, .xls) or CSV (.csv) file' }, + { status: 400 } + ); + } + + // Validate file size (10MB max) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + return NextResponse.json( + { message: 'File size too large. Maximum size is 10MB' }, + { status: 400 } + ); + } + + // Read file content + const buffer = await file.arrayBuffer(); + let data: any[] = []; + + if (file.type === 'text/csv') { + // Handle CSV file + const text = new TextDecoder().decode(buffer); + const lines = text.split('\n').filter(line => line.trim()); + + if (lines.length < 2) { + return NextResponse.json( + { message: 'File must contain at least header and one data row' }, + { status: 400 } + ); + } + + const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '')); + + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(',').map(v => v.trim().replace(/"/g, '')); + const row: any = {}; + headers.forEach((header, index) => { + row[header] = values[index] || ''; + }); + data.push(row); + } + } else { + // Handle Excel file + const workbook = XLSX.read(buffer, { type: 'buffer' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + data = XLSX.utils.sheet_to_json(worksheet); + } + + if (data.length === 0) { + return NextResponse.json( + { message: 'No data found in file' }, + { status: 400 } + ); + } + + // Normalize column names and validate required columns + const normalizedData = data.map(row => { + const normalizedRow: any = {}; + Object.keys(row).forEach(key => { + const normalizedKey = key.toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, ''); + normalizedRow[normalizedKey] = row[key]; + }); + return normalizedRow; + }); + + // Map common column variations to standard names + const columnMappings = { + 'nim': ['nim', 'NIM', 'Nim'], + 'kode_mk': ['kode_mk', 'Kode MK', 'kode mk', 'kodemk', 'kodeMK'], + 'semester': ['semester', 'Semester', 'sem'], + 'nilai_huruf': ['nilai_huruf', 'Nilai Huruf', 'nilai huruf', 'nilaihuruf', 'nilaiHuruf', 'grade'], + 'nilai_angka': ['nilai_angka', 'Nilai Angka', 'nilai angka', 'nilaiangka', 'nilaiAngka', 'score'] + }; + + // Further normalize data with column mappings + const finalData = normalizedData.map(row => { + const finalRow: any = {}; + + Object.keys(columnMappings).forEach(standardKey => { + const variations = columnMappings[standardKey as keyof typeof columnMappings]; + let value = null; + + // Try to find the value using different variations + for (const variation of variations) { + const normalizedVariation = variation.toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, ''); + + if (row[normalizedVariation] !== undefined && row[normalizedVariation] !== null && row[normalizedVariation] !== '') { + value = row[normalizedVariation]; + break; + } + } + + finalRow[standardKey] = value; + }); + + return finalRow; + }); + + // Validate required columns + const requiredColumns = ['nim', 'kode_mk', 'semester', 'nilai_huruf', 'nilai_angka']; + const firstRow = finalData[0]; + const missingColumns = requiredColumns.filter(col => + firstRow[col] === undefined || firstRow[col] === null || firstRow[col] === '' + ); + + if (missingColumns.length > 0) { + return NextResponse.json( + { message: `Missing required columns: ${missingColumns.join(', ')}. Please check your Excel headers.` }, + { status: 400 } + ); + } + + // Use the normalized data for processing + data = finalData; + + // Process and validate data + const processedData: NilaiMahasiswaUpload[] = []; + const errors: string[] = []; + + for (let i = 0; i < data.length; i++) { + const row = data[i]; + const rowNum = i + 2; // +2 because Excel rows start from 1 and we skip header + + try { + // Validate and convert data types + const semester = parseInt(row.semester); + + // Handle decimal comma (Indonesian format) and convert to dot + const nilaiAngkaStr = row.nilai_angka.toString().replace(',', '.'); + const nilai_angka = parseFloat(nilaiAngkaStr); + + if (isNaN(semester) || semester <= 0 || semester > 8) { + errors.push(`Row ${rowNum}: Semester must be a number between 1-8`); + continue; + } + + if (isNaN(nilai_angka) || nilai_angka < 0 || nilai_angka > 4) { + errors.push(`Row ${rowNum}: Nilai angka must be a number between 0-4 (current: ${row.nilai_angka})`); + continue; + } + + // Validate nilai_huruf + const validNilaiHuruf = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'E']; + if (!validNilaiHuruf.includes(row.nilai_huruf)) { + errors.push(`Row ${rowNum}: nilai_huruf must be one of: ${validNilaiHuruf.join(', ')}`); + continue; + } + + // Validate required fields + if (!row.nim || !row.kode_mk) { + errors.push(`Row ${rowNum}: nim and kode_mk are required`); + continue; + } + + // Keep kode_mk as is - don't normalize it + let kodeMK = row.kode_mk.toString().trim(); + + processedData.push({ + nim: row.nim.toString().trim(), + kode_mk: kodeMK, + semester, + nilai_huruf: row.nilai_huruf as 'A' | 'B+' | 'B' | 'C+' | 'C' | 'D+' | 'D' | 'E', + nilai_angka + }); + } catch (error) { + errors.push(`Row ${rowNum}: Invalid data format`); + } + } + + if (errors.length > 0) { + return NextResponse.json( + { + message: 'Validation errors found', + errors: errors.slice(0, 10) // Limit to first 10 errors + }, + { status: 400 } + ); + } + + // Get existing mahasiswa and mata kuliah for validation + const { data: existingMahasiswa, error: fetchMahasiswaError } = await supabase + .from('mahasiswa') + .select('id_mahasiswa, nim'); + + if (fetchMahasiswaError) { + return NextResponse.json( + { message: 'Failed to validate mahasiswa data' }, + { status: 500 } + ); + } + + const { data: existingMataKuliah, error: fetchMKError } = await supabase + .from('mata_kuliah') + .select('id_mk, kode_mk'); + + if (fetchMKError) { + return NextResponse.json( + { message: 'Failed to validate mata kuliah data' }, + { status: 500 } + ); + } + + const nimToIdMap = new Map(existingMahasiswa?.map(m => [m.nim, m.id_mahasiswa]) || []); + const kodeMKToIdMap = new Map(existingMataKuliah?.map(mk => [mk.kode_mk, mk.id_mk]) || []); + + // Process data for insertion + const insertData: any[] = []; + const validationErrors: string[] = []; + + for (const nilai of processedData) { + // Check if mahasiswa exists + const id_mahasiswa = nimToIdMap.get(nilai.nim); + if (!id_mahasiswa) { + validationErrors.push(`NIM ${nilai.nim} not found in database`); + continue; + } + + // Check if mata kuliah exists + const id_mk = kodeMKToIdMap.get(nilai.kode_mk); + if (!id_mk) { + validationErrors.push(`Kode MK ${nilai.kode_mk} not found in database`); + continue; + } + + insertData.push({ + id_mahasiswa, + id_mk, + nilai_huruf: nilai.nilai_huruf, + nilai_angka: nilai.nilai_angka, + semester: nilai.semester + }); + } + + if (validationErrors.length > 0) { + return NextResponse.json( + { + message: 'Data validation errors', + errors: validationErrors.slice(0, 10) + }, + { status: 400 } + ); + } + + // Insert data to database + let successCount = 0; + if (insertData.length > 0) { + const { error: insertError } = await supabase + .from('nilai_mahasiswa') + .insert(insertData); + + if (insertError) { + return NextResponse.json( + { message: 'Failed to insert data to database' }, + { status: 500 } + ); + } + + successCount = insertData.length; + } + + return NextResponse.json({ + message: 'Upload completed', + successCount, + totalRows: processedData.length + }); + + } catch (error) { + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/keloladata/upload-mata-kuliah/route.ts b/app/api/keloladata/upload-mata-kuliah/route.ts new file mode 100644 index 0000000..452f41b --- /dev/null +++ b/app/api/keloladata/upload-mata-kuliah/route.ts @@ -0,0 +1,257 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; +import * as XLSX from 'xlsx'; + +interface MataKuliahUpload { + kode_mk: string; + nama_mk: string; + sks: number; + semester: number; + jenis_mk: 'Wajib' | 'Pilihan Wajib' | 'Pilihan'; + kode_prasyarat?: string; +} + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const file = formData.get('file') as File; + + if (!file) { + return NextResponse.json( + { message: 'No file uploaded' }, + { status: 400 } + ); + } + + // Validate file type + const validTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv' + ]; + + if (!validTypes.includes(file.type)) { + return NextResponse.json( + { message: 'Invalid file type. Please upload Excel (.xlsx, .xls) or CSV (.csv) file' }, + { status: 400 } + ); + } + + // Validate file size (10MB max) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + return NextResponse.json( + { message: 'File size too large. Maximum size is 10MB' }, + { status: 400 } + ); + } + + // Read file content + const buffer = await file.arrayBuffer(); + let data: any[] = []; + + if (file.type === 'text/csv') { + // Handle CSV file + const text = new TextDecoder().decode(buffer); + const lines = text.split('\n').filter(line => line.trim()); + + if (lines.length < 2) { + return NextResponse.json( + { message: 'File must contain at least header and one data row' }, + { status: 400 } + ); + } + + const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '')); + + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(',').map(v => v.trim().replace(/"/g, '')); + const row: any = {}; + headers.forEach((header, index) => { + row[header] = values[index] || ''; + }); + data.push(row); + } + } else { + // Handle Excel file + const workbook = XLSX.read(buffer, { type: 'buffer' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + data = XLSX.utils.sheet_to_json(worksheet); + } + + if (data.length === 0) { + return NextResponse.json( + { message: 'No data found in file' }, + { status: 400 } + ); + } + + // Validate required columns + const requiredColumns = ['kode_mk', 'nama_mk', 'sks', 'semester', 'jenis_mk']; + const firstRow = data[0]; + const missingColumns = requiredColumns.filter(col => !(col in firstRow)); + + if (missingColumns.length > 0) { + return NextResponse.json( + { message: `Missing required columns: ${missingColumns.join(', ')}` }, + { status: 400 } + ); + } + + // Process and validate data + const processedData: MataKuliahUpload[] = []; + const errors: string[] = []; + + for (let i = 0; i < data.length; i++) { + const row = data[i]; + const rowNum = i + 2; // +2 because Excel rows start from 1 and we skip header + + try { + // Validate and convert data types + const sks = parseInt(row.sks); + const semester = parseInt(row.semester); + + if (isNaN(sks) || sks <= 0 || sks > 6) { + errors.push(`Row ${rowNum}: SKS must be a number between 1-6`); + continue; + } + + if (isNaN(semester) || semester <= 0 || semester > 8) { + errors.push(`Row ${rowNum}: Semester must be a number between 1-8`); + continue; + } + + // Validate jenis_mk + const validJenisMK = ['Wajib', 'Pilihan Wajib', 'Pilihan']; + if (!validJenisMK.includes(row.jenis_mk)) { + errors.push(`Row ${rowNum}: jenis_mk must be one of: ${validJenisMK.join(', ')}`); + continue; + } + + // Validate required fields + if (!row.kode_mk || !row.nama_mk) { + errors.push(`Row ${rowNum}: kode_mk and nama_mk are required`); + continue; + } + + processedData.push({ + kode_mk: row.kode_mk.toString().trim(), + nama_mk: row.nama_mk.toString().trim(), + sks, + semester, + jenis_mk: row.jenis_mk as 'Wajib' | 'Pilihan Wajib' | 'Pilihan', + kode_prasyarat: row.kode_prasyarat ? row.kode_prasyarat.toString().trim() : undefined + }); + } catch (error) { + errors.push(`Row ${rowNum}: Invalid data format`); + } + } + + if (errors.length > 0) { + return NextResponse.json( + { + message: 'Validation errors found', + errors: errors.slice(0, 10) // Limit to first 10 errors + }, + { status: 400 } + ); + } + + // Get existing mata kuliah for duplicate check and prasyarat validation + const { data: existingMK, error: fetchError } = await supabase + .from('mata_kuliah') + .select('kode_mk, id_mk'); + + if (fetchError) { + console.error('Error fetching existing mata kuliah:', fetchError); + return NextResponse.json( + { message: 'Failed to validate data' }, + { status: 500 } + ); + } + + const existingCodes = new Set(existingMK?.map(mk => mk.kode_mk) || []); + const codeToIdMap = new Map(existingMK?.map(mk => [mk.kode_mk, mk.id_mk]) || []); + + // Process data for insertion + const insertData: any[] = []; + const duplicates: string[] = []; + + for (const mk of processedData) { + // Check for duplicates + if (existingCodes.has(mk.kode_mk)) { + duplicates.push(mk.kode_mk); + continue; + } + + // Resolve prasyarat ID + let id_prasyarat = null; + if (mk.kode_prasyarat) { + id_prasyarat = codeToIdMap.get(mk.kode_prasyarat); + if (!id_prasyarat) { + errors.push(`Prasyarat ${mk.kode_prasyarat} not found for ${mk.kode_mk}`); + continue; + } + } + + insertData.push({ + kode_mk: mk.kode_mk, + nama_mk: mk.nama_mk, + sks: mk.sks, + semester: mk.semester, + jenis_mk: mk.jenis_mk, + id_prasyarat + }); + } + + if (errors.length > 0) { + return NextResponse.json( + { + message: 'Prasyarat validation errors', + errors: errors.slice(0, 10) + }, + { status: 400 } + ); + } + + // Insert data to database + let successCount = 0; + if (insertData.length > 0) { + const { error: insertError } = await supabase + .from('mata_kuliah') + .insert(insertData); + + if (insertError) { + console.error('Error inserting mata kuliah:', insertError); + return NextResponse.json( + { message: 'Failed to insert data to database' }, + { status: 500 } + ); + } + + successCount = insertData.length; + } + + // Prepare response + const response: any = { + message: 'Upload completed', + successCount, + totalRows: processedData.length + }; + + if (duplicates.length > 0) { + response.duplicates = duplicates; + response.duplicateCount = duplicates.length; + } + + return NextResponse.json(response); + + } catch (error) { + console.error('Error processing upload:', error); + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/asal-daerah/route.ts b/app/api/tabeldetail/asal-daerah/route.ts new file mode 100644 index 0000000..d4dc476 --- /dev/null +++ b/app/api/tabeldetail/asal-daerah/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaAsalDaerah { + nim: string; + nama: string; + tahun_angkatan: number; + kabupaten: string; +} + +// GET - Ambil data mahasiswa dengan asal kabupaten dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select(` + nim, + nama, + tahun_angkatan, + kabupaten + `) + .not('kabupaten', 'is', null) // Hanya ambil mahasiswa yang memiliki data kabupaten + .order('nama', { ascending: true }); + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { message: 'Failed to fetch data', error: error.message }, + { status: 500 } + ); + } + + // Transform data untuk memastikan format yang konsisten + const transformedData: MahasiswaAsalDaerah[] = (data || []).map((item: any) => ({ + nim: item.nim || '', + nama: item.nama || '', + tahun_angkatan: item.tahun_angkatan || 0, + kabupaten: item.kabupaten || '' + })); + + return NextResponse.json(transformedData, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/asal-daerah API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/asal-provinsi/route.ts b/app/api/tabeldetail/asal-provinsi/route.ts new file mode 100644 index 0000000..44a1fb6 --- /dev/null +++ b/app/api/tabeldetail/asal-provinsi/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaAsalProvinsi { + nim: string; + nama: string; + tahun_angkatan: number; + provinsi: string; +} + +// GET - Ambil data mahasiswa dengan asal provinsi dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select(` + nim, + nama, + tahun_angkatan, + provinsi + `) + .not('provinsi', 'is', null) // Hanya ambil mahasiswa yang memiliki data provinsi + .order('nama', { ascending: true }); + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { message: 'Failed to fetch data', error: error.message }, + { status: 500 } + ); + } + + // Transform data untuk memastikan format yang konsisten + const transformedData: MahasiswaAsalProvinsi[] = (data || []).map((item: any) => ({ + nim: item.nim || '', + nama: item.nama || '', + tahun_angkatan: item.tahun_angkatan || 0, + provinsi: item.provinsi || '' + })); + + return NextResponse.json(transformedData, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/asal-provinsi API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/bimbingan-dosen/route.ts b/app/api/tabeldetail/bimbingan-dosen/route.ts new file mode 100644 index 0000000..c7f3708 --- /dev/null +++ b/app/api/tabeldetail/bimbingan-dosen/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaBimbinganDosen { + nim: string; + nama: string; + tahun_angkatan: number; + nama_pembimbing_1: string | null; + nama_pembimbing_2: string | null; + status_bimbingan: string; +} + +// GET - Ambil data mahasiswa dengan pembimbing dan filter tahun angkatan serta nama dosen untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const namaDosen = searchParams.get('nama_dosen'); + + let query = supabase + .from('mahasiswa') + .select(` + nim, + nama, + tahun_angkatan, + status_bimbingan, + pembimbing_1, + pembimbing_2, + dosen_pembimbing_1:dosen!pembimbing_1(nama_dosen), + dosen_pembimbing_2:dosen!pembimbing_2(nama_dosen) + `) + .or('pembimbing_1.not.is.null,pembimbing_2.not.is.null') // Hanya ambil mahasiswa yang memiliki minimal satu pembimbing + .order('nama', { ascending: true }); + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { message: 'Failed to fetch data', error: error.message }, + { status: 500 } + ); + } + + // Transform data untuk meratakan field yang di-join + let transformedData: MahasiswaBimbinganDosen[] = (data || []).map((item: any) => ({ + nim: item.nim || '', + nama: item.nama || '', + tahun_angkatan: item.tahun_angkatan || 0, + nama_pembimbing_1: item.dosen_pembimbing_1?.nama_dosen || null, + nama_pembimbing_2: item.dosen_pembimbing_2?.nama_dosen || null, + status_bimbingan: item.status_bimbingan || '' + })); + + // Filter berdasarkan nama dosen jika diberikan + if (namaDosen && namaDosen !== 'all') { + transformedData = transformedData.filter(mahasiswa => + mahasiswa.nama_pembimbing_1 === namaDosen || + mahasiswa.nama_pembimbing_2 === namaDosen + ); + } + + return NextResponse.json(transformedData, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/bimbingan-dosen API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/nama-beasiswa/route.ts b/app/api/tabeldetail/nama-beasiswa/route.ts new file mode 100644 index 0000000..918ced7 --- /dev/null +++ b/app/api/tabeldetail/nama-beasiswa/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaNamaBeasiswa { + nim: string; + nama: string; + tahun_angkatan: number; + nama_beasiswa: string; +} + +// GET - Ambil data mahasiswa dengan nama beasiswa dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('beasiswa_mahasiswa') + .select(` + nama_beasiswa, + mahasiswa!inner( + nim, + nama, + tahun_angkatan + ) + `) + .order('nama_beasiswa', { ascending: true }); + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { message: 'Failed to fetch data', error: error.message }, + { status: 500 } + ); + } + + // Transform data untuk meratakan field yang di-join + const transformedData: MahasiswaNamaBeasiswa[] = (data || []).map((item: any) => ({ + nim: item.mahasiswa?.nim || '', + nama: item.mahasiswa?.nama || '', + tahun_angkatan: item.mahasiswa?.tahun_angkatan || 0, + nama_beasiswa: item.nama_beasiswa || '' + })); + + // Urutkan berdasarkan nama mahasiswa + transformedData.sort((a, b) => a.nama.localeCompare(b.nama)); + + return NextResponse.json(transformedData, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/nama-beasiswa API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/tingkat-prestasi/route.ts b/app/api/tabeldetail/tingkat-prestasi/route.ts new file mode 100644 index 0000000..cfff14d --- /dev/null +++ b/app/api/tabeldetail/tingkat-prestasi/route.ts @@ -0,0 +1,72 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaTingkatPrestasi { + nim: string; + nama: string; + tahun_angkatan: number; + tingkat_prestasi: string; + nama_prestasi: string; +} + +// GET - Ambil data mahasiswa dengan tingkat prestasi dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('prestasi_mahasiswa') + .select(` + tingkat_prestasi, + nama_prestasi, + mahasiswa!inner( + nim, + nama, + tahun_angkatan + ) + `) + .order('tingkat_prestasi', { ascending: true }); + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { message: 'Failed to fetch data', error: error.message }, + { status: 500 } + ); + } + + // Transform data untuk meratakan field yang di-join + const transformedData: MahasiswaTingkatPrestasi[] = (data || []).map((item: any) => ({ + nim: item.mahasiswa?.nim || '', + nama: item.mahasiswa?.nama || '', + tahun_angkatan: item.mahasiswa?.tahun_angkatan || 0, + tingkat_prestasi: item.tingkat_prestasi || '', + nama_prestasi: item.nama_prestasi || '' + })); + + // Urutkan berdasarkan nama mahasiswa + transformedData.sort((a, b) => a.nama.localeCompare(b.nama)); + + return NextResponse.json(transformedData, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/tingkat-prestasi API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/detail/asal-daerah/page.tsx b/app/detail/asal-daerah/page.tsx index 4046958..22bbf1d 100644 --- a/app/detail/asal-daerah/page.tsx +++ b/app/detail/asal-daerah/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import AsalDaerahChart from "@/components/charts/AsalDaerahChart"; import AsalDaerahPerAngkatanChart from "@/components/charts/AsalDaerahPerAngkatanChart"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelAsalDaerahMahasiswa from "@/components/chartstable/tabelasaldaerahmahasiswa"; export default function AsalDaerahDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -36,6 +37,9 @@ export default function AsalDaerahDetailPage() { )} + {/* Tabel Section */} + + {/* Information Section */}

@@ -50,6 +54,7 @@ export default function AsalDaerahDetailPage() {
  • • Menampilkan distribusi mahasiswa berdasarkan asal kabupaten/kota
  • • Data geografis menunjukkan sebaran mahasiswa dari berbagai daerah
  • • Grafik horizontal bar chart untuk kemudahan membaca nama daerah
  • +
  • • Data dapat di-download dan dianalisis per daerah
  • {selectedYear !== "all" && ( diff --git a/app/detail/asal-provinsi/page.tsx b/app/detail/asal-provinsi/page.tsx index 7029217..a1dd6be 100644 --- a/app/detail/asal-provinsi/page.tsx +++ b/app/detail/asal-provinsi/page.tsx @@ -1,8 +1,13 @@ 'use client'; +import { useState } from 'react'; import ProvinsiMahasiswaPieChart from "@/components/chartsDashboard/ProvinsiMahasiswaPieChart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelAsalProvinsiMahasiswa from "@/components/chartstable/tabelasalprovinsi mahasiswa"; export default function AsalProvinsiDetailPage() { + const [selectedYear, setSelectedYear] = useState('all'); + return (
    @@ -17,6 +22,17 @@ export default function AsalProvinsiDetailPage() {
    + {/* Filter Section */} +
    + +
    + + {/* Table Section */} + + {/* Information Section */}

    diff --git a/app/detail/bimbingan-dosen/page.tsx b/app/detail/bimbingan-dosen/page.tsx index 01a2919..1978924 100644 --- a/app/detail/bimbingan-dosen/page.tsx +++ b/app/detail/bimbingan-dosen/page.tsx @@ -4,9 +4,12 @@ import { useState } from "react"; import BimbinganDosenChart from "@/components/charts/BimbinganDosenChart"; import BimbinganDosenPerAngkatanChart from "@/components/charts/BimbinganDosenPerAngkatanChart"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import FilterNamaDosen from "@/components/ui/filter-nama-dosen"; +import TabelBimbinganDosenMahasiswa from "@/components/chartstable/tabelbimbingandosenmahasiswa"; export default function BimbinganDosenDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); + const [selectedDosen, setSelectedDosen] = useState("all"); return (
    @@ -36,6 +39,20 @@ export default function BimbinganDosenDetailPage() { )}
    + {/* Filter Nama Dosen Section */} +
    + +
    + + {/* Tabel Section */} + + {/* Information Section */}

    @@ -50,6 +67,7 @@ export default function BimbinganDosenDetailPage() {
  • • Menampilkan statistik bimbingan mahasiswa per dosen pembimbing
  • • Data terbagi menjadi dua kategori: "Selesai" dan "Belum Selesai"
  • • Hijau menunjukkan bimbingan selesai, kuning untuk belum selesai
  • +
  • • Data dapat di-download dan dianalisis per dosen
  • {selectedYear !== "all" && ( diff --git a/app/detail/nama-beasiswa/page.tsx b/app/detail/nama-beasiswa/page.tsx index 6f50547..c532a7b 100644 --- a/app/detail/nama-beasiswa/page.tsx +++ b/app/detail/nama-beasiswa/page.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import NamaBeasiswaDashChart from "@/components/chartsDashboard/NamaBeasiswaDashChart"; -import NamaBeasiswaDashPieChartPerangkatan from "@/components/chartsDashboard/NamaBeasiswaDashPieChartPerangkatan"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelNamaBeasiswaMahasiswa from "@/components/chartstable/tabelnamabeasiswamahasiswa"; export default function NamaBeasiswaDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -11,6 +11,8 @@ export default function NamaBeasiswaDetailPage() { return (
    + {/* Header Section */} + {/* Filter Section */} ) : ( <> - )}
    + {/* Tabel Section */} + + {/* Information Section */}

    @@ -45,13 +52,13 @@ export default function NamaBeasiswaDetailPage() {

    - Grafik Utama (Nama Beasiswa) + Grafik Utama (Semua Angkatan)

      -
    • • Menampilkan distribusi mahasiswa berdasarkan jenis beasiswa per tahun angkatan
    • -
    • • Data dikategorikan berdasarkan nama program beasiswa yang diterima
    • -
    • • Grafik bar chart yang menunjukkan jumlah penerima per jenis beasiswa
    • -
    • • Visualisasi tren penerimaan beasiswa dari waktu ke waktu
    • +
    • • Menampilkan distribusi nama beasiswa mahasiswa per tahun angkatan
    • +
    • • Data dikategorikan berdasarkan jenis beasiswa yang diterima
    • +
    • • Grafik batang horizontal yang menunjukkan jumlah penerima per beasiswa
    • +
    • • Data dapat di-download dan dianalisis per kategori
    {selectedYear !== "all" && ( @@ -60,10 +67,10 @@ export default function NamaBeasiswaDetailPage() { Grafik Per Angkatan ({selectedYear})

      -
    • • Menampilkan proporsi penerima beasiswa per jenis untuk angkatan {selectedYear}
    • -
    • • Grafik pie chart dengan persentase per nama beasiswa
    • +
    • • Menampilkan distribusi beasiswa untuk angkatan {selectedYear}
    • +
    • • Grafik batang horizontal dengan persentase per jenis beasiswa
    • • Data spesifik untuk tahun angkatan yang dipilih
    • -
    • • Memberikan insight detail distribusi beasiswa per angkatan
    • +
    • • Memberikan insight detail penerima beasiswa per angkatan
    )} @@ -72,4 +79,4 @@ export default function NamaBeasiswaDetailPage() {

    ); -} +} \ No newline at end of file diff --git a/app/detail/tingkat-prestasi/page.tsx b/app/detail/tingkat-prestasi/page.tsx index 6fa6655..a28e502 100644 --- a/app/detail/tingkat-prestasi/page.tsx +++ b/app/detail/tingkat-prestasi/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import TingkatPrestasiDashChart from "@/components/chartsDashboard/TingkatPrestasiDashChart"; import TingkatPrestasiPieChart from "@/components/chartsDashboard/TingkatPrestasiPieChartDash"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelTingkatPrestasiMahasiswa from "@/components/chartstable/tabeltingkatprestasimahasiswa"; export default function TingkatPrestasiDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -37,6 +38,9 @@ export default function TingkatPrestasiDetailPage() { )} + {/* Tabel Section */} + + {/* Information Section */}

    @@ -50,8 +54,8 @@ export default function TingkatPrestasiDetailPage() {
    • • Menampilkan distribusi mahasiswa berprestasi berdasarkan tingkat prestasi per tahun angkatan
    • • Data dikategorikan berdasarkan tingkat/level prestasi yang diraih mahasiswa
    • -
    • • Grafik bar chart yang menunjukkan jumlah mahasiswa per tingkat prestasi
    • -
    • • Visualisasi tren pencapaian prestasi dari waktu ke waktu
    • +
    • • Grafik batang horizontal yang menunjukkan jumlah mahasiswa per tingkat prestasi
    • +
    • • Data dapat di-download dan dianalisis per kategori

    {selectedYear !== "all" && ( @@ -60,7 +64,7 @@ export default function TingkatPrestasiDetailPage() { Grafik Per Angkatan ({selectedYear})
      -
    • • Menampilkan proporsi mahasiswa per tingkat prestasi untuk angkatan {selectedYear}
    • +
    • • Menampilkan distribusi prestasi untuk angkatan {selectedYear}
    • • Grafik pie chart dengan persentase per tingkat prestasi
    • • Data spesifik untuk tahun angkatan yang dipilih
    • • Memberikan insight detail distribusi prestasi per angkatan
    • diff --git a/app/keloladata/matakuliah/page.tsx b/app/keloladata/matakuliah/page.tsx new file mode 100644 index 0000000..217f062 --- /dev/null +++ b/app/keloladata/matakuliah/page.tsx @@ -0,0 +1,9 @@ +import DataTableMataKuliah from "@/components/datatable/data-table-mata-kuliah"; + +export default function MataKuliahPage() { + return ( +
      + +
      + ); +} diff --git a/app/keloladata/nilaimahasiswa/page.tsx b/app/keloladata/nilaimahasiswa/page.tsx new file mode 100644 index 0000000..7d8bb30 --- /dev/null +++ b/app/keloladata/nilaimahasiswa/page.tsx @@ -0,0 +1,9 @@ +import DataTableNilaiMahasiswa from "@/components/datatable/data-table-nilai-mahasiswa"; + +export default function NilaiMahasiswaPage() { + return ( +
      + +
      + ); +} diff --git a/components/charts/AsalDaerahChart.tsx b/components/charts/AsalDaerahChart.tsx index a4c8a1b..be2254f 100644 --- a/components/charts/AsalDaerahChart.tsx +++ b/components/charts/AsalDaerahChart.tsx @@ -56,8 +56,15 @@ export default function AsalDaerahChart({ }, dataLabels: { enabled: true, - formatter: function (val: number) { - return val.toString(); + formatter: function (val: number, opts: any) { + // Hitung total dari semua data + const allData = opts.w.config.series[0].data; + const total = allData.reduce((sum: number, value: number) => sum + value, 0); + + if (total === 0 || val === 0) return '0%'; + + const percentage = ((val / total) * 100).toFixed(1); + return percentage + '%'; }, style: { fontSize: '12px', @@ -127,6 +134,16 @@ export default function AsalDaerahChart({ }, dataLabels: { ...prev.dataLabels, + formatter: function (val: number, opts: any) { + // Hitung total dari semua data + const allData = opts.w.config.series[0].data; + const total = allData.reduce((sum: number, value: number) => sum + value, 0); + + if (total === 0 || val === 0) return '0%'; + + const percentage = ((val / total) * 100).toFixed(0); + return percentage + '%'; + }, style: { ...prev.dataLabels?.style, colors: [theme === 'dark' ? '#fff' : '#000'] @@ -197,9 +214,15 @@ export default function AsalDaerahChart({ setData(result); + // Sort data dari terbesar ke terkecil + const sortedData = result.sort((a, b) => b.jumlah - a.jumlah); + + // Hitung total untuk persentase + const totalMahasiswa = sortedData.reduce((sum, item) => sum + item.jumlah, 0); + // Proses data untuk chart - const kabupaten = result.map(item => item.kabupaten); - const jumlah = result.map(item => item.jumlah); + const kabupaten = sortedData.map(item => item.kabupaten); + const jumlah = sortedData.map(item => item.jumlah); setSeries([{ name: 'Jumlah Mahasiswa', diff --git a/components/charts/BimbinganDosenChart.tsx b/components/charts/BimbinganDosenChart.tsx index 76612e0..b4e4d41 100644 --- a/components/charts/BimbinganDosenChart.tsx +++ b/components/charts/BimbinganDosenChart.tsx @@ -132,10 +132,68 @@ export default function BimbinganDosenChart({ colors: ['#10B981', '#F59E0B'], // Hijau untuk Selesai, Kuning untuk Belum Selesai tooltip: { theme: theme === 'dark' ? 'dark' : 'light', - y: { - formatter: function (val: number) { - return val + " mahasiswa"; - } + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const dosen = w.globals.labels[dataPointIndex]; + const isDark = theme === 'dark'; + + // Hitung total untuk dosen ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const statusNames = ['Selesai', 'Belum Selesai']; + const statusColors = ['#10B981', '#F59E0B']; + + let tooltipContent = ` +
      +
      ${dosen}
      `; + + // Tambahkan setiap status + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + tooltipContent += ` +
      +
      + ${statusNames[index]} + ${value} +
      `; + }); + + // Tambahkan total + tooltipContent += ` +
      +
      + Total + ${total} +
      +
      + `; + + return tooltipContent; } } }); @@ -225,8 +283,8 @@ export default function BimbinganDosenChart({ throw new Error('Format data tidak valid diterima dari server'); } - // Urutkan data berdasarkan nama dosen A-Z - const sortedResult = result.sort((a, b) => a.nama_dosen.localeCompare(b.nama_dosen)); + // Urutkan data berdasarkan kategori "Belum Selesai" dari terbanyak ke terkecil + const sortedResult = result.sort((a, b) => b.belum_selesai - a.belum_selesai); setData(sortedResult); // Proses data untuk chart diff --git a/components/charts/BimbinganDosenPerAngkatanChart.tsx b/components/charts/BimbinganDosenPerAngkatanChart.tsx index 87bc98a..890677a 100644 --- a/components/charts/BimbinganDosenPerAngkatanChart.tsx +++ b/components/charts/BimbinganDosenPerAngkatanChart.tsx @@ -151,10 +151,68 @@ export default function BimbinganDosenPerAngkatanChart({ tahunAngkatan }: Props) colors: ['#10B981', '#F59E0B'], // Hijau untuk Selesai, Kuning untuk Belum Selesai tooltip: { theme: theme === 'dark' ? 'dark' : 'light', - y: { - formatter: function (val: number) { - return val + " mahasiswa"; - } + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const dosen = w.globals.labels[dataPointIndex]; + const isDark = theme === 'dark'; + + // Hitung total untuk dosen ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const statusNames = ['Selesai', 'Belum Selesai']; + const statusColors = ['#10B981', '#F59E0B']; + + let tooltipContent = ` +
      +
      ${dosen}
      `; + + // Tambahkan setiap status + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + tooltipContent += ` +
      +
      + ${statusNames[index]} + ${value} +
      `; + }); + + // Tambahkan total + tooltipContent += ` +
      +
      + Total + ${total} +
      +
      + `; + + return tooltipContent; } } }); @@ -244,8 +302,8 @@ export default function BimbinganDosenPerAngkatanChart({ tahunAngkatan }: Props) throw new Error('Format data tidak valid diterima dari server'); } - // Urutkan data berdasarkan nama dosen A-Z - const sortedResult = result.sort((a, b) => a.nama_dosen.localeCompare(b.nama_dosen)); + // Urutkan data berdasarkan kategori "Belum Selesai" dari terbanyak ke terkecil + const sortedResult = result.sort((a, b) => b.belum_selesai - a.belum_selesai); setData(sortedResult); // Proses data untuk chart diff --git a/components/chartsDashboard/NamaBeasiswaDashChart.tsx b/components/chartsDashboard/NamaBeasiswaDashChart.tsx index 8a915d2..cb9d445 100644 --- a/components/chartsDashboard/NamaBeasiswaDashChart.tsx +++ b/components/chartsDashboard/NamaBeasiswaDashChart.tsx @@ -36,6 +36,7 @@ export default function NamaBeasiswaDashChart({ const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [chartOptions, setChartOptions] = useState({}); useEffect(() => { const fetchData = async () => { @@ -63,6 +64,10 @@ export default function NamaBeasiswaDashChart({ ); setData(sortedData); + + // Update chart options dengan categories yang benar + const years = [...new Set(sortedData.map(item => item.tahun_angkatan))].sort(); + updateChartOptions(years); } catch (err) { console.error('Error in fetchData:', err); setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); @@ -72,7 +77,198 @@ export default function NamaBeasiswaDashChart({ }; fetchData(); - }, [selectedYear]); + }, [selectedYear, theme]); + + // Initialize chart options on mount + useEffect(() => { + updateChartOptions([]); + }, []); + + // Function to update chart options + const updateChartOptions = (years: number[]) => { + console.log('Updating chart options with years:', years); + setChartOptions({ + chart: { + type: 'bar', + stacked: true, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + barHeight: '70%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number, opts: any) { + const seriesIndex = opts.seriesIndex; + const dataPointIndex = opts.dataPointIndex; + + // Hitung total untuk tahun angkatan ini + const allSeries = opts.w.config.series; + let totalForYear = 0; + allSeries.forEach((series: any) => { + totalForYear += series.data[dataPointIndex] || 0; + }); + + if (totalForYear === 0 || val === 0) return '0%'; + + const percentage = ((val / totalForYear) * 100).toFixed(0); + return percentage + '%'; + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: years.map(y => y.toString()), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + min: 0, + tickAmount: 5 + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#EC4899', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const tahun = w.globals.labels[dataPointIndex]; + const isDark = theme === 'dark'; + + // Hitung total untuk tahun ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const beasiswaNames = w.config.series.map((s: any) => s.name); + const beasiswaColors = w.config.colors; + + let tooltipContent = ` +
      +
      Angkatan ${tahun}
      `; + + // Tambahkan setiap jenis beasiswa + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + tooltipContent += ` +
      +
      + ${beasiswaNames[index]} + ${value} +
      `; + }); + + // Tambahkan total + tooltipContent += ` +
      +
      + Total + ${total} +
      +
      + `; + + return tooltipContent; + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }); + }; // Process data for series const processSeriesData = () => { @@ -90,129 +286,6 @@ export default function NamaBeasiswaDashChart({ })); }; - const chartOptions: ApexOptions = { - chart: { - type: 'bar', - stacked: false, - toolbar: { - show: true, - tools: { - download: true, - selection: true, - zoom: true, - zoomin: true, - zoomout: true, - pan: true, - reset: true - } - }, - background: theme === 'dark' ? '#0F172B' : '#fff', - }, - plotOptions: { - bar: { - horizontal: false, - columnWidth: '55%', - borderRadius: 1, - }, - }, - dataLabels: { - enabled: true, - formatter: function (val: number) { - return val.toString(); - }, - style: { - fontSize: '12px', - colors: [theme === 'dark' ? '#fff' : '#000'] - } - }, - stroke: { - show: true, - width: 2, - colors: ['transparent'] - }, - xaxis: { - categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), - title: { - text: 'Tahun Angkatan', - style: { - fontSize: '14px', - fontWeight: 'bold', - color: theme === 'dark' ? '#fff' : '#000' - } - }, - labels: { - style: { - fontSize: '12px', - colors: theme === 'dark' ? '#fff' : '#000' - } - }, - axisBorder: { - show: true, - color: theme === 'dark' ? '#374151' : '#E5E7EB' - }, - axisTicks: { - show: true, - color: theme === 'dark' ? '#374151' : '#E5E7EB' - }, - tickAmount: 5, - }, - yaxis: { - title: { - text: 'Jumlah Mahasiswa', - style: { - fontSize: '14px', - fontWeight: 'bold', - color: theme === 'dark' ? '#fff' : '#000' - } - }, - labels: { - style: { - fontSize: '12px', - colors: theme === 'dark' ? '#fff' : '#000' - } - }, - axisBorder: { - show: true, - color: theme === 'dark' ? '#374151' : '#E5E7EB' - } - }, - fill: { - opacity: 1 - }, - colors: ['#3B82F6', '#EC4899', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4'], - tooltip: { - theme: theme === 'dark' ? 'dark' : 'light', - y: { - formatter: function (val: number) { - return val + " mahasiswa"; - } - } - }, - legend: { - position: 'top', - fontSize: '14px', - markers: { - size: 12, - }, - itemMargin: { - horizontal: 10, - }, - labels: { - colors: theme === 'dark' ? '#fff' : '#000' - } - }, - grid: { - borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', - strokeDashArray: 4, - padding: { - top: 20, - right: 0, - bottom: 0, - left: 0 - } - } - }; - const series = processSeriesData(); if (loading) { @@ -271,7 +344,7 @@ export default function NamaBeasiswaDashChart({
      - {typeof window !== 'undefined' && series.length > 0 && ( + {typeof window !== 'undefined' && series.length > 0 && Object.keys(chartOptions).length > 0 && ( { + totalForYear += series.data[dataPointIndex] || 0; + }); + + if (totalForYear === 0 || val === 0) return '0%'; + + const percentage = ((val / totalForYear) * 100).toFixed(0); + return percentage + '%'; }, style: { fontSize: '12px', @@ -143,15 +155,7 @@ export default function TingkatPrestasiDashChart({ fontSize: '12px', colors: theme === 'dark' ? '#fff' : '#000' } - }, - axisBorder: { - show: true, - color: theme === 'dark' ? '#374151' : '#E5E7EB' - }, - axisTicks: { - show: true, - color: theme === 'dark' ? '#374151' : '#E5E7EB' - }, + } }, yaxis: { title: { @@ -168,10 +172,7 @@ export default function TingkatPrestasiDashChart({ colors: theme === 'dark' ? '#fff' : '#000' } }, - axisBorder: { - show: true, - color: theme === 'dark' ? '#374151' : '#E5E7EB' - }, + min: 0, tickAmount: 5 }, fill: { @@ -180,10 +181,68 @@ export default function TingkatPrestasiDashChart({ colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], tooltip: { theme: theme === 'dark' ? 'dark' : 'light', - y: { - formatter: function (val: number) { - return val + " mahasiswa"; - } + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const tahun = w.globals.labels[dataPointIndex]; + const isDark = theme === 'dark'; + + // Hitung total untuk tahun ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const tingkatNames = w.config.series.map((s: any) => s.name); + const tingkatColors = w.config.colors; + + let tooltipContent = ` +
      +
      Angkatan ${tahun}
      `; + + // Tambahkan setiap tingkat prestasi + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + tooltipContent += ` +
      +
      + ${tingkatNames[index]} + ${value} +
      `; + }); + + // Tambahkan total + tooltipContent += ` +
      +
      + Total + ${total} +
      +
      + `; + + return tooltipContent; } }, legend: { diff --git a/components/chartsDashboard/kkdashboardchart.tsx b/components/chartsDashboard/kkdashboardchart.tsx index 2ac32fe..4ef959d 100644 --- a/components/chartsDashboard/kkdashboardchart.tsx +++ b/components/chartsDashboard/kkdashboardchart.tsx @@ -98,8 +98,21 @@ export default function KelompokKeahlianStatusChart({ }, dataLabels: { enabled: true, - formatter: function (val: number) { - return val.toString(); + formatter: function (val: number, opts: any) { + const seriesIndex = opts.seriesIndex; + const dataPointIndex = opts.dataPointIndex; + + // Hitung total untuk tahun angkatan ini + const allSeries = opts.w.config.series; + let totalForYear = 0; + allSeries.forEach((series: any) => { + totalForYear += series.data[dataPointIndex] || 0; + }); + + if (totalForYear === 0 || val === 0) return '0%'; + + const percentage = ((val / totalForYear) * 100).toFixed(0); + return percentage + '%'; }, style: { fontSize: '12px', @@ -163,10 +176,70 @@ export default function KelompokKeahlianStatusChart({ colors: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0', '#8B5CF6', '#EC4899', '#06B6D4', '#F97316'], tooltip: { theme: theme === 'dark' ? 'dark' : 'light', - y: { - formatter: function (val: number) { - return val + ' mahasiswa'; - } + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const tahun = w.globals.labels[dataPointIndex]; + + // Hitung total untuk tahun ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const isDark = theme === 'dark'; + + let tooltipContent = ` +
      +
      Angkatan ${tahun}
      + `; + + // Tampilkan setiap kelompok keahlian + w.config.series.forEach((seriesItem: any, index: number) => { + const value = series[index][dataPointIndex] || 0; + const color = w.config.colors[index]; + + tooltipContent += ` +
      +
      + ${seriesItem.name} + ${value} +
      + `; + }); + + // Tampilkan total + tooltipContent += ` +
      +
      + Total + ${total} +
      +
      + `; + + return tooltipContent; } } }; diff --git a/components/chartstable/tabelasaldaerahmahasiswa.tsx b/components/chartstable/tabelasaldaerahmahasiswa.tsx new file mode 100644 index 0000000..fc221ca --- /dev/null +++ b/components/chartstable/tabelasaldaerahmahasiswa.tsx @@ -0,0 +1,378 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Loader2, MapPin } from "lucide-react"; + +interface MahasiswaAsalDaerah { + nim: string; + nama: string; + tahun_angkatan: number; + kabupaten: string; +} + +interface TabelAsalDaerahMahasiswaProps { + selectedYear: string; +} + +export default function TabelAsalDaerahMahasiswa({ selectedYear }: TabelAsalDaerahMahasiswaProps) { + const [mahasiswaData, setMahasiswaData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [paginatedData, setPaginatedData] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = selectedYear === 'all' + ? '/api/tabeldetail/asal-daerah' + : `/api/tabeldetail/asal-daerah?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa asal daerah data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Hitung statistik berdasarkan kabupaten + const kabupatenStats = mahasiswaData.reduce((acc, mahasiswa) => { + const kabupaten = mahasiswa.kabupaten; + acc[kabupaten] = (acc[kabupaten] || 0) + 1; + return acc; + }, {} as Record); + + // Hitung statistik tahun angkatan + const tahunAngkatanStats = mahasiswaData.reduce((acc, mahasiswa) => { + const tahun = mahasiswa.tahun_angkatan; + acc[tahun] = (acc[tahun] || 0) + 1; + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_kabupaten: Object.keys(kabupatenStats).length, + total_tahun_angkatan: Object.keys(tahunAngkatanStats).length + }; + + if (loading) { + return ( + + + +
      + + Loading... +
      +
      +
      +
      + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Asal Daerah Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
      +
      +
      Total Mahasiswa
      +
      {stats.total}
      +
      +
      +
      Total Kabupaten
      +
      {stats.total_kabupaten}
      +
      +
      +
      Total Tahun Angkatan
      +
      {stats.total_tahun_angkatan}
      +
      +
      + {/* Tampilkan ringkasan kabupaten terbanyak */} +
      + {Object.entries(kabupatenStats) + .sort(([,a], [,b]) => b - a) // Urutkan berdasarkan jumlah mahasiswa terbanyak + .slice(0, 5) // Ambil 5 kabupaten teratas + .map(([kabupaten, count]) => ( + + {kabupaten}: {count} + + ))} + {Object.keys(kabupatenStats).length > 5 && ( + + +{Object.keys(kabupatenStats).length - 5} lainnya + + )} +
      +
      + + {/* Show entries selector */} +
      + Show + + entries +
      + +
      + + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Asal Kabupaten + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => ( + + + {(currentPage - 1) * pageSize + index + 1} + + + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.kabupaten} + + + )) + )} + +
      +
      + + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
      +
      + Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
      + + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
      + )} +
      +
      + ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/chartstable/tabelasalprovinsi mahasiswa.tsx b/components/chartstable/tabelasalprovinsi mahasiswa.tsx new file mode 100644 index 0000000..923f1e4 --- /dev/null +++ b/components/chartstable/tabelasalprovinsi mahasiswa.tsx @@ -0,0 +1,372 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Loader2, MapPin } from "lucide-react"; + +interface MahasiswaAsalProvinsi { + nim: string; + nama: string; + tahun_angkatan: number; + provinsi: string; +} + +interface TabelAsalProvinsiMahasiswaProps { + selectedYear: string; +} + +export default function TabelAsalProvinsiMahasiswa({ selectedYear }: TabelAsalProvinsiMahasiswaProps) { + const [mahasiswaData, setMahasiswaData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [paginatedData, setPaginatedData] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = selectedYear === 'all' + ? '/api/tabeldetail/asal-provinsi' + : `/api/tabeldetail/asal-provinsi?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa asal provinsi data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Hitung statistik provinsi + const provinsiStats = mahasiswaData.reduce((acc, mahasiswa) => { + const provinsi = mahasiswa.provinsi; + acc[provinsi] = (acc[provinsi] || 0) + 1; + return acc; + }, {} as Record); + + // Hitung statistik tahun angkatan + const tahunAngkatanStats = mahasiswaData.reduce((acc, mahasiswa) => { + const tahun = mahasiswa.tahun_angkatan; + acc[tahun] = (acc[tahun] || 0) + 1; + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_provinsi: Object.keys(provinsiStats).length, + total_tahun_angkatan: Object.keys(tahunAngkatanStats).length + }; + + if (loading) { + return ( + + + +
      + + Loading... +
      +
      +
      +
      + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Asal Provinsi Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
      +
      +
      Total Mahasiswa
      +
      {stats.total}
      +
      +
      +
      Total Provinsi
      +
      {stats.total_provinsi}
      +
      +
      +
      Total Tahun Angkatan
      +
      {stats.total_tahun_angkatan}
      +
      +
      + {/* Tampilkan ringkasan provinsi */} +
      + {Object.entries(provinsiStats) + .sort(([,a], [,b]) => b - a) // Urutkan berdasarkan jumlah mahasiswa terbanyak + .map(([provinsi, count]) => ( + + {provinsi}: {count} + + ))} +
      +
      + + {/* Show entries selector */} +
      + Show + + entries +
      + +
      + + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Provinsi + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => ( + + + {(currentPage - 1) * pageSize + index + 1} + + + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.provinsi} + + + )) + )} + +
      +
      + + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
      +
      + Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
      + + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
      + )} +
      +
      + ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/chartstable/tabelbimbingandosenmahasiswa.tsx b/components/chartstable/tabelbimbingandosenmahasiswa.tsx new file mode 100644 index 0000000..1dcd507 --- /dev/null +++ b/components/chartstable/tabelbimbingandosenmahasiswa.tsx @@ -0,0 +1,417 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Loader2, Users } from "lucide-react"; + +interface MahasiswaBimbinganDosen { + nim: string; + nama: string; + tahun_angkatan: number; + nama_pembimbing_1: string | null; + nama_pembimbing_2: string | null; + status_bimbingan: string; +} + +interface TabelBimbinganDosenMahasiswaProps { + selectedYear: string; + selectedDosen: string; +} + +export default function TabelBimbinganDosenMahasiswa({ selectedYear, selectedDosen }: TabelBimbinganDosenMahasiswaProps) { + const [mahasiswaData, setMahasiswaData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [paginatedData, setPaginatedData] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + let url = '/api/tabeldetail/bimbingan-dosen?'; + const params = new URLSearchParams(); + + if (selectedYear !== 'all') { + params.append('tahun_angkatan', selectedYear); + } + + if (selectedDosen !== 'all') { + params.append('nama_dosen', selectedDosen); + } + + url += params.toString(); + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa bimbingan dosen data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedDosen]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Fungsi untuk mendapatkan styling berdasarkan status bimbingan + const getStatusBimbinganStyle = (status: string) => { + switch (status) { + case "Selesai": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"; + case "Belum Selesai": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"; + } + }; + + // Hitung statistik berdasarkan status bimbingan + const statusStats = mahasiswaData.reduce((acc, mahasiswa) => { + const status = mahasiswa.status_bimbingan; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + // Hitung statistik tahun angkatan + const tahunAngkatanStats = mahasiswaData.reduce((acc, mahasiswa) => { + const tahun = mahasiswa.tahun_angkatan; + acc[tahun] = (acc[tahun] || 0) + 1; + return acc; + }, {} as Record); + + // Hitung statistik dosen pembimbing + const dosenStats = mahasiswaData.reduce((acc, mahasiswa) => { + if (mahasiswa.nama_pembimbing_1) { + acc[mahasiswa.nama_pembimbing_1] = (acc[mahasiswa.nama_pembimbing_1] || 0) + 1; + } + if (mahasiswa.nama_pembimbing_2) { + acc[mahasiswa.nama_pembimbing_2] = (acc[mahasiswa.nama_pembimbing_2] || 0) + 1; + } + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_status: Object.keys(statusStats).length, + total_tahun_angkatan: Object.keys(tahunAngkatanStats).length, + total_dosen: Object.keys(dosenStats).length + }; + + if (loading) { + return ( + + + +
      + + Loading... +
      +
      +
      +
      + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Bimbingan Dosen Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + {selectedDosen !== 'all' && ` - ${selectedDosen}`} + + {/* Tampilkan ringkasan statistik */} +
      +
      +
      Total Mahasiswa
      +
      {stats.total}
      +
      +
      +
      Total Tahun Angkatan
      +
      {stats.total_tahun_angkatan}
      +
      +
      + {/* Tampilkan ringkasan status bimbingan */} +
      + {Object.entries(statusStats) + .sort(([,a], [,b]) => b - a) // Urutkan berdasarkan jumlah mahasiswa terbanyak + .map(([status, count]) => ( + + {status}: {count} + + ))} +
      +
      + + {/* Show entries selector */} +
      + Show + + entries +
      + +
      + + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Pembimbing 1 + Pembimbing 2 + Status Bimbingan + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => ( + + + {(currentPage - 1) * pageSize + index + 1} + + + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.nama_pembimbing_1 || '-'} + + + {mahasiswa.nama_pembimbing_2 || '-'} + + + + {mahasiswa.status_bimbingan} + + + + )) + )} + +
      +
      + + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
      +
      + Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
      + + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
      + )} +
      +
      + ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/chartstable/tabelnamabeasiswamahasiswa.tsx b/components/chartstable/tabelnamabeasiswamahasiswa.tsx new file mode 100644 index 0000000..2700b18 --- /dev/null +++ b/components/chartstable/tabelnamabeasiswamahasiswa.tsx @@ -0,0 +1,372 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Loader2, Award } from "lucide-react"; + +interface MahasiswaNamaBeasiswa { + nim: string; + nama: string; + tahun_angkatan: number; + nama_beasiswa: string; +} + +interface TabelNamaBeasiswaMahasiswaProps { + selectedYear: string; +} + +export default function TabelNamaBeasiswaMahasiswa({ selectedYear }: TabelNamaBeasiswaMahasiswaProps) { + const [mahasiswaData, setMahasiswaData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [paginatedData, setPaginatedData] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = selectedYear === 'all' + ? '/api/tabeldetail/nama-beasiswa' + : `/api/tabeldetail/nama-beasiswa?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa nama beasiswa data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Hitung statistik beasiswa + const beasiswaStats = mahasiswaData.reduce((acc, mahasiswa) => { + const beasiswa = mahasiswa.nama_beasiswa; + acc[beasiswa] = (acc[beasiswa] || 0) + 1; + return acc; + }, {} as Record); + + // Hitung statistik tahun angkatan + const tahunAngkatanStats = mahasiswaData.reduce((acc, mahasiswa) => { + const tahun = mahasiswa.tahun_angkatan; + acc[tahun] = (acc[tahun] || 0) + 1; + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_beasiswa: Object.keys(beasiswaStats).length, + total_tahun_angkatan: Object.keys(tahunAngkatanStats).length + }; + + if (loading) { + return ( + + + +
      + + Loading... +
      +
      +
      +
      + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Nama Beasiswa Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
      +
      +
      Total Mahasiswa
      +
      {stats.total}
      +
      +
      +
      Total Jenis Beasiswa
      +
      {stats.total_beasiswa}
      +
      +
      +
      Total Tahun Angkatan
      +
      {stats.total_tahun_angkatan}
      +
      +
      + {/* Tampilkan ringkasan beasiswa */} +
      + {Object.entries(beasiswaStats) + .sort(([,a], [,b]) => b - a) // Urutkan berdasarkan jumlah mahasiswa terbanyak + .map(([beasiswa, count]) => ( + + {beasiswa}: {count} + + ))} +
      +
      + + {/* Show entries selector */} +
      + Show + + entries +
      + +
      + + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Nama Beasiswa + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => ( + + + {(currentPage - 1) * pageSize + index + 1} + + + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.nama_beasiswa} + + + )) + )} + +
      +
      + + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
      +
      + Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
      + + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
      + )} +
      +
      + ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/chartstable/tabeltingkatprestasimahasiswa.tsx b/components/chartstable/tabeltingkatprestasimahasiswa.tsx new file mode 100644 index 0000000..9949411 --- /dev/null +++ b/components/chartstable/tabeltingkatprestasimahasiswa.tsx @@ -0,0 +1,397 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Loader2, Trophy } from "lucide-react"; + +interface MahasiswaTingkatPrestasi { + nim: string; + nama: string; + tahun_angkatan: number; + tingkat_prestasi: string; + nama_prestasi: string; +} + +interface TabelTingkatPrestasiMahasiswaProps { + selectedYear: string; +} + +export default function TabelTingkatPrestasiMahasiswa({ selectedYear }: TabelTingkatPrestasiMahasiswaProps) { + const [mahasiswaData, setMahasiswaData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [paginatedData, setPaginatedData] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = selectedYear === 'all' + ? '/api/tabeldetail/tingkat-prestasi' + : `/api/tabeldetail/tingkat-prestasi?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa tingkat prestasi data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Fungsi untuk mendapatkan styling berdasarkan tingkat prestasi + const getTingkatPrestasiStyle = (tingkat: string) => { + switch (tingkat) { + case "Internasional": + return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300"; + case "Nasional": + return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"; + case "Provinsi": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"; + case "Kabupaten": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"; + } + }; + + // Hitung statistik berdasarkan tingkat prestasi + const tingkatStats = mahasiswaData.reduce((acc, mahasiswa) => { + const tingkat = mahasiswa.tingkat_prestasi; + acc[tingkat] = (acc[tingkat] || 0) + 1; + return acc; + }, {} as Record); + + // Hitung statistik tahun angkatan + const tahunAngkatanStats = mahasiswaData.reduce((acc, mahasiswa) => { + const tahun = mahasiswa.tahun_angkatan; + acc[tahun] = (acc[tahun] || 0) + 1; + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_tingkat: Object.keys(tingkatStats).length, + total_tahun_angkatan: Object.keys(tahunAngkatanStats).length + }; + + if (loading) { + return ( + + + +
      + + Loading... +
      +
      +
      +
      + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Tingkat Prestasi Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
      +
      +
      Total Mahasiswa
      +
      {stats.total}
      +
      +
      +
      Total Tingkat Prestasi
      +
      {stats.total_tingkat}
      +
      +
      +
      Total Tahun Angkatan
      +
      {stats.total_tahun_angkatan}
      +
      +
      + {/* Tampilkan ringkasan tingkat prestasi */} +
      + {Object.entries(tingkatStats) + .sort(([,a], [,b]) => b - a) // Urutkan berdasarkan jumlah mahasiswa terbanyak + .map(([tingkat, count]) => ( + + {tingkat}: {count} + + ))} +
      +
      + + {/* Show entries selector */} +
      + Show + + entries +
      + +
      + + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Nama Prestasi + Tingkat Prestasi + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => ( + + + {(currentPage - 1) * pageSize + index + 1} + + + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.nama_prestasi} + + + + {mahasiswa.tingkat_prestasi} + + + + )) + )} + +
      +
      + + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
      +
      + Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
      + + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
      + )} +
      +
      + ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/datatable/data-table-mata-kuliah.tsx b/components/datatable/data-table-mata-kuliah.tsx new file mode 100644 index 0000000..2db1753 --- /dev/null +++ b/components/datatable/data-table-mata-kuliah.tsx @@ -0,0 +1,787 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { + PlusCircle, + Pencil, + Trash2, + Search, + X, + Loader2, + BookOpen +} from "lucide-react"; +import UploadFileMataKuliah from "@/components/datatable/upload-file-mata-kuliah"; +import { useToast } from "@/components/ui/toast-provider"; + +// Define the MataKuliah type +interface MataKuliah { + id_mk: number; + kode_mk: string; + nama_mk: string; + sks: number; + semester: number; + jenis_mk: 'Wajib' | 'Pilihan Wajib' | 'Pilihan'; + id_prasyarat: number | null; + nama_prasyarat: string | null; + created_at: string; + updated_at: string; +} + +export default function DataTableMataKuliah() { + const { showSuccess, showError } = useToast(); + // State for data + const [mataKuliah, setMataKuliah] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for filtering + const [searchTerm, setSearchTerm] = useState(""); + const [filterSemester, setFilterSemester] = useState(""); + const [filterJenisMK, setFilterJenisMK] = useState(""); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [paginatedData, setPaginatedData] = useState([]); + + // State for form + const [formMode, setFormMode] = useState<"add" | "edit">("add"); + const [formData, setFormData] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + // State for delete confirmation + const [deleteId, setDeleteId] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + // State for prasyarat options + const [prasyaratOptions, setPrasyaratOptions] = useState([]); + + // Fetch data on component mount + useEffect(() => { + fetchMataKuliah(); + }, []); + + // Filter data when search term or filter changes + useEffect(() => { + filterData(); + }, [searchTerm, filterSemester, filterJenisMK, mataKuliah]); + + // Update paginated data when filtered data or pagination settings change + useEffect(() => { + paginateData(); + }, [filteredData, currentPage, pageSize]); + + // Fetch mata kuliah data from API + const fetchMataKuliah = async () => { + try { + setLoading(true); + let url = "/api/keloladata/data-mata-kuliah"; + + // Add filters to URL if they exist + const params = new URLSearchParams(); + if (searchTerm) { + params.append("search", searchTerm); + } + if (filterSemester && filterSemester !== "all") { + params.append("semester", filterSemester); + } + + if (params.toString()) { + url += `?${params.toString()}`; + } + + const response = await fetch(url); + + if (!response.ok) { + throw new Error("Failed to fetch data"); + } + + const data = await response.json(); + setMataKuliah(data); + setFilteredData(data); + setPrasyaratOptions(data); // Set options for prasyarat dropdown + setError(null); + } catch (err) { + setError("Error fetching data. Please try again later."); + console.error("Error fetching data:", err); + } finally { + setLoading(false); + } + }; + + // Filter data based on search term and filters + const filterData = () => { + let filtered = [...mataKuliah]; + + // Filter by search term + if (searchTerm) { + filtered = filtered.filter( + (item) => + (item.kode_mk && item.kode_mk.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.nama_mk && item.nama_mk.toLowerCase().includes(searchTerm.toLowerCase())) + ); + } + + // Filter by semester + if (filterSemester && filterSemester !== "all") { + filtered = filtered.filter((item) => item.semester === parseInt(filterSemester)); + } + + // Filter by jenis mata kuliah + if (filterJenisMK && filterJenisMK !== "all") { + filtered = filtered.filter((item) => item.jenis_mk === filterJenisMK); + } + + setFilteredData(filtered); + // Reset to first page when filters change + setCurrentPage(1); + }; + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(filteredData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(filteredData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Reset form data + const resetForm = () => { + setFormData({ + jenis_mk: 'Wajib' // Set default value + }); + }; + + // Handle form input changes + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // Handle select input changes + const handleSelectChange = (name: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [name]: value === "none" || value === "" ? null : (name === 'id_prasyarat' ? parseInt(value) : value) + })); + }; + + // Open form dialog for adding new mata kuliah + const handleAdd = () => { + setFormMode("add"); + resetForm(); + setIsDialogOpen(true); + }; + + // Open form dialog for editing mata kuliah + const handleEdit = (data: MataKuliah) => { + setFormMode("edit"); + setFormData(data); + setIsDialogOpen(true); + }; + + // Open delete confirmation dialog + const handleDeleteConfirm = (id: number) => { + setDeleteId(id); + setIsDeleteDialogOpen(true); + }; + + // Submit form for add/edit + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + setIsSubmitting(true); + + if (formMode === "add") { + // Add new mata kuliah + const response = await fetch("/api/keloladata/data-mata-kuliah", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + showError("Gagal!", responseData.message || "Failed to add mata kuliah"); + throw new Error(responseData.message || "Failed to add mata kuliah"); + } + + showSuccess("Berhasil!", "Mata kuliah berhasil ditambahkan"); + } else { + // Edit existing mata kuliah + const response = await fetch(`/api/keloladata/data-mata-kuliah?id=${formData.id_mk}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + showError("Gagal!", responseData.message || "Failed to update mata kuliah"); + throw new Error(responseData.message || "Failed to update mata kuliah"); + } + + showSuccess("Berhasil!", "Mata kuliah berhasil diperbarui"); + } + + // Refresh data after successful operation + await fetchMataKuliah(); + setIsDialogOpen(false); + resetForm(); + } catch (err) { + console.error("Error submitting form:", err); + } finally { + setIsSubmitting(false); + } + }; + + // Delete mata kuliah + const handleDelete = async () => { + if (!deleteId) return; + + try { + setIsDeleting(true); + + const response = await fetch(`/api/keloladata/data-mata-kuliah?id=${deleteId}`, { + method: "DELETE", + }); + + const responseData = await response.json(); + + if (!response.ok) { + showError("Gagal!", responseData.message || "Failed to delete mata kuliah"); + throw new Error(responseData.message || "Failed to delete mata kuliah"); + } + + // Refresh data after successful deletion + await fetchMataKuliah(); + setIsDeleteDialogOpen(false); + setDeleteId(null); + showSuccess("Berhasil!", "Mata kuliah berhasil dihapus"); + } catch (err) { + console.error("Error deleting mata kuliah:", err); + } finally { + setIsDeleting(false); + } + }; + + // Generate pagination items + const renderPaginationItems = () => { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + }; + + // Calculate the range of entries being displayed + const getDisplayRange = () => { + if (filteredData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, filteredData.length); + + return { start, end }; + }; + + // Get badge color based on semester + const getSemesterBadgeColor = (semester: number) => { + const colors = [ + "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300", + "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300", + "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300", + "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", + "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300", + "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300", + "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300", + "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300" + ]; + return colors[(semester - 1) % colors.length] || "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"; + }; + + // Get badge color based on jenis mata kuliah + const getJenisMKBadgeColor = (jenis: string) => { + switch (jenis) { + case "Wajib": + return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"; + case "Pilihan Wajib": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"; + case "Pilihan": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"; + } + }; + + return ( +
      +
      +

      Data Mata Kuliah

      +
      + + +
      +
      + + {/* Filters */} +
      +
      + + setSearchTerm(e.target.value)} + /> + {searchTerm && ( + setSearchTerm("")} + /> + )} +
      + + +
      + + {/* Show entries selector */} +
      + Show + + entries +
      + + {/* Table */} + {loading ? ( +
      + +
      + ) : error ? ( +
      + {error} +
      + ) : ( +
      + + + + Kode MK + Nama Mata Kuliah + SKS + Semester + Jenis MK + Prasyarat + Aksi + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang sesuai dengan filter + + + ) : ( + paginatedData.map((mk) => ( + + {mk.kode_mk} + {mk.nama_mk} + {mk.sks} + + + Semester {mk.semester} + + + + + {mk.jenis_mk} + + + + {mk.nama_prasyarat || '-'} + + +
      + + +
      +
      +
      + )) + )} +
      +
      +
      + )} + + {/* Pagination info and controls */} + {!loading && !error && filteredData.length > 0 && ( +
      +
      + Showing {getDisplayRange().start} to {getDisplayRange().end} of {filteredData.length} entries +
      + + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
      + )} + + {/* Add/Edit Dialog */} + + + + + {formMode === "add" ? "Tambah Mata Kuliah" : "Edit Mata Kuliah"} + + +
      +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + + + + + + +
      +
      +
      + + {/* Delete Confirmation Dialog */} + + + + Konfirmasi Hapus + +
      +

      Apakah Anda yakin ingin menghapus mata kuliah ini?

      +

      + Tindakan ini tidak dapat dibatalkan. +

      +
      + + + + + + +
      +
      +
      + ); +} diff --git a/components/datatable/data-table-nilai-mahasiswa.tsx b/components/datatable/data-table-nilai-mahasiswa.tsx new file mode 100644 index 0000000..faf1fdf --- /dev/null +++ b/components/datatable/data-table-nilai-mahasiswa.tsx @@ -0,0 +1,835 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { + PlusCircle, + Pencil, + Trash2, + Search, + X, + Loader2, + GraduationCap +} from "lucide-react"; +import { useToast } from "@/components/ui/toast-provider"; +import UploadFileNilaiMahasiswa from "./upload-file-nilai-mahasiswa"; + +// Define the NilaiMahasiswa type +interface NilaiMahasiswa { + id_nilai: number; + id_mahasiswa: number; + id_mk: number; + nim: string; + nama: string; + kode_mk: string; + nama_mk: string; + nilai_huruf: "A" | "B+" | "B" | "C+" | "C" | "D+" | "D" | "E"; + nilai_angka: number; + semester: number; + created_at: string; + updated_at: string; +} + +// Define MataKuliah type for dropdown +interface MataKuliah { + id_mk: number; + kode_mk: string; + nama_mk: string; +} + +export default function DataTableNilaiMahasiswa() { + const { showSuccess, showError } = useToast(); + // State for data + const [nilaiMahasiswa, setNilaiMahasiswa] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [mataKuliahList, setMataKuliahList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for filtering + const [searchTerm, setSearchTerm] = useState(""); + const [filterSemester, setFilterSemester] = useState(""); + const [filterNilaiHuruf, setFilterNilaiHuruf] = useState(""); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [paginatedData, setPaginatedData] = useState([]); + + // State for form + const [formMode, setFormMode] = useState<"add" | "edit">("add"); + const [formData, setFormData] = useState>({ + nilai_huruf: "A", + nilai_angka: 4.0, + semester: 1 + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + // State for delete confirmation + const [deleteId, setDeleteId] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + + // Fetch data on component mount + useEffect(() => { + fetchNilaiMahasiswa(); + fetchMataKuliah(); + }, []); + + // Filter data when search term or filter changes + useEffect(() => { + filterData(); + }, [searchTerm, filterSemester, filterNilaiHuruf, nilaiMahasiswa]); + + // Update paginated data when filtered data or pagination settings change + useEffect(() => { + paginateData(); + }, [filteredData, currentPage, pageSize]); + + // Fetch mata kuliah for dropdown + const fetchMataKuliah = async () => { + try { + const response = await fetch("/api/keloladata/data-mata-kuliah"); + if (response.ok) { + const data = await response.json(); + setMataKuliahList(data); + } + } catch (err) { + console.error("Error fetching mata kuliah:", err); + } + }; + + // Fetch nilai mahasiswa data from API + const fetchNilaiMahasiswa = async () => { + try { + setLoading(true); + let url = "/api/keloladata/data-nilai-mahasiswa"; + + // Add filters to URL if they exist + const params = new URLSearchParams(); + if (searchTerm) { + params.append("search", searchTerm); + } + if (filterSemester && filterSemester !== "all") { + params.append("semester", filterSemester); + } + if (filterNilaiHuruf && filterNilaiHuruf !== "all") { + params.append("nilai_huruf", filterNilaiHuruf); + } + + if (params.toString()) { + url += `?${params.toString()}`; + } + + const response = await fetch(url); + + if (!response.ok) { + throw new Error("Failed to fetch data"); + } + + const data = await response.json(); + setNilaiMahasiswa(data); + setFilteredData(data); + setError(null); + } catch (err) { + setError("Error fetching data. Please try again later."); + console.error("Error fetching data:", err); + } finally { + setLoading(false); + } + }; + + // Filter data based on search term and filters + const filterData = () => { + let filtered = [...nilaiMahasiswa]; + + // Filter by search term + if (searchTerm) { + filtered = filtered.filter( + (item) => + (item.nim && item.nim.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.nama && item.nama.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.kode_mk && item.kode_mk.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.nama_mk && item.nama_mk.toLowerCase().includes(searchTerm.toLowerCase())) + ); + } + + // Filter by semester + if (filterSemester && filterSemester !== "all") { + filtered = filtered.filter((item) => item.semester === parseInt(filterSemester)); + } + + // Filter by nilai huruf + if (filterNilaiHuruf && filterNilaiHuruf !== "all") { + filtered = filtered.filter((item) => item.nilai_huruf === filterNilaiHuruf); + } + + setFilteredData(filtered); + // Reset to first page when filters change + setCurrentPage(1); + }; + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(filteredData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(filteredData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Reset form data + const resetForm = () => { + setFormData({ + nilai_huruf: "A", + nilai_angka: 4.0, + semester: 1 + }); + }; + + // Handle form input changes + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // Handle select input changes + const handleSelectChange = (name: string, value: string) => { + setFormData((prev) => ({ ...prev, [name]: value })); + + // Auto-update nilai_angka based on nilai_huruf + if (name === "nilai_huruf") { + const nilaiAngkaMap: Record = { + "A": 4.0, + "B+": 3.5, + "B": 3.0, + "C+": 2.5, + "C": 2.0, + "D+": 1.5, + "D": 1.0, + "E": 0.0 + }; + setFormData((prev) => ({ + ...prev, + [name]: value as NilaiMahasiswa["nilai_huruf"], + nilai_angka: nilaiAngkaMap[value] || 0 + })); + } + }; + + // Open form dialog for adding new nilai + const handleAdd = () => { + setFormMode("add"); + resetForm(); + setIsDialogOpen(true); + }; + + // Open form dialog for editing nilai + const handleEdit = (data: NilaiMahasiswa) => { + setFormMode("edit"); + setFormData(data); + setIsDialogOpen(true); + }; + + // Open delete confirmation dialog + const handleDeleteConfirm = (id: number) => { + setDeleteId(id); + setIsDeleteDialogOpen(true); + }; + + // Submit form for add/edit + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + setIsSubmitting(true); + + if (formMode === "add") { + // Add new nilai + const response = await fetch("/api/keloladata/data-nilai-mahasiswa", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + // Handle specific NIM not found error + if (response.status === 404 && responseData.message.includes("tidak terdaftar")) { + showError("Gagal!", `NIM ${formData.nim} tidak terdaftar`); + throw new Error(`NIM ${formData.nim} tidak terdaftar dalam database. Silakan cek kembali NIM yang dimasukkan.`); + } + showError("Gagal!", responseData.message || "Failed to add nilai"); + throw new Error(responseData.message || "Failed to add nilai"); + } + + // Show success message with student info + showSuccess("Berhasil!", "Nilai mahasiswa berhasil ditambahkan"); + } else { + // Edit existing nilai + const response = await fetch(`/api/keloladata/data-nilai-mahasiswa?id=${formData.id_nilai}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + // Handle specific NIM not found error + if (response.status === 404 && responseData.message.includes("tidak terdaftar")) { + showError("Gagal!", `NIM ${formData.nim} tidak terdaftar`); + throw new Error(`NIM ${formData.nim} tidak terdaftar dalam database. Silakan cek kembali NIM yang dimasukkan.`); + } + showError("Gagal!", responseData.message || "Failed to update nilai"); + throw new Error(responseData.message || "Failed to update nilai"); + } + + showSuccess("Berhasil!", "Nilai mahasiswa berhasil diperbarui"); + } + + // Refresh data after successful operation + await fetchNilaiMahasiswa(); + setIsDialogOpen(false); + resetForm(); + } catch (err) { + console.error("Error submitting form:", err); + } finally { + setIsSubmitting(false); + } + }; + + // Delete nilai + const handleDelete = async () => { + if (!deleteId) return; + + try { + setIsDeleting(true); + + const response = await fetch(`/api/keloladata/data-nilai-mahasiswa?id=${deleteId}`, { + method: "DELETE", + }); + + if (!response.ok) { + const errorData = await response.json(); + showError("Gagal!", errorData.message || "Failed to delete nilai"); + throw new Error(errorData.message || "Failed to delete nilai"); + } + + // Refresh data after successful deletion + await fetchNilaiMahasiswa(); + setIsDeleteDialogOpen(false); + setDeleteId(null); + showSuccess("Berhasil!", "Nilai mahasiswa berhasil dihapus"); + } catch (err) { + console.error("Error deleting nilai:", err); + } finally { + setIsDeleting(false); + } + }; + + + // Generate pagination items + const renderPaginationItems = () => { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="dark:text-white" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="dark:text-white" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="dark:text-white" + > + {totalPages} + + + ); + } + + return items; + }; + + // Calculate the range of entries being displayed + const getDisplayRange = () => { + if (filteredData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, filteredData.length); + + return { start, end }; + }; + + // Get badge color based on nilai huruf + const getNilaiBadgeColor = (nilai: string) => { + switch (nilai) { + case "A": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; + case "B+": + return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200"; + case "B": + return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"; + case "C+": + return "bg-cyan-100 text-cyan-800 dark:bg-cyan-900 dark:text-cyan-200"; + case "C": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"; + case "D+": + return "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200"; + case "D": + return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"; + case "E": + return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"; + } + }; + + return ( +
      +
      +

      Data Nilai Mahasiswa

      +
      + + +
      +
      + + {/* Filters */} +
      +
      + + setSearchTerm(e.target.value)} + /> + {searchTerm && ( + setSearchTerm("")} + /> + )} +
      + + +
      + + {/* Show entries selector */} +
      + Show + + entries +
      + + {/* Table */} + {loading ? ( +
      + +
      + ) : error ? ( +
      + {error} +
      + ) : ( +
      + + + + NIM + Nama Mahasiswa + Kode MK + Nama Mata Kuliah + Semester + Nilai Huruf + Nilai Angka + Aksi + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang sesuai dengan filter + + + ) : ( + paginatedData.map((nilai) => ( + + {nilai.nim} + {nilai.nama} + {nilai.kode_mk} + {nilai.nama_mk} + + + Semester {nilai.semester} + + + + + {nilai.nilai_huruf} + + + {nilai.nilai_angka.toFixed(2)} + +
      + + +
      +
      +
      + )) + )} +
      +
      +
      + )} + + {/* Pagination info and controls */} + {!loading && !error && filteredData.length > 0 && ( +
      +
      + Showing {getDisplayRange().start} to {getDisplayRange().end} of {filteredData.length} entries +
      + + + + handlePageChange(Math.max(1, currentPage - 1))} + className={`dark:text-white ${currentPage === 1 ? "pointer-events-none opacity-50" : ""}`} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={`dark:text-white ${currentPage === getTotalPages() ? "pointer-events-none opacity-50" : ""}`} + /> + + + +
      + )} + + {/* Add/Edit Dialog */} + + + + + {formMode === "add" ? "Tambah Nilai" : "Edit Nilai"} + + +
      +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
      + + + + + + +
      +
      +
      + + {/* Delete Confirmation Dialog */} + + + + Konfirmasi Hapus + +
      +

      Apakah Anda yakin ingin menghapus data nilai ini?

      +

      + Tindakan ini tidak dapat dibatalkan. +

      +
      + + + + + + +
      +
      + +
      + ); +} diff --git a/components/datatable/upload-file-mata-kuliah.tsx b/components/datatable/upload-file-mata-kuliah.tsx new file mode 100644 index 0000000..f228783 --- /dev/null +++ b/components/datatable/upload-file-mata-kuliah.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose +} from "@/components/ui/dialog"; +import { Upload, FileSpreadsheet, Loader2, Download } from "lucide-react"; +import { useToast } from "@/components/ui/toast-provider"; + +interface UploadFileMataKuliahProps { + onUploadSuccess: () => void; +} + +export default function UploadFileMataKuliah({ onUploadSuccess }: UploadFileMataKuliahProps) { + const { showSuccess, showError } = useToast(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + // Handle file selection + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Validate file type + const validTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv' + ]; + + if (!validTypes.includes(file.type)) { + showError("Error!", "File harus berformat Excel (.xlsx, .xls) atau CSV (.csv)"); + return; + } + + setSelectedFile(file); + } + }; + + // Handle file upload + const handleUpload = async () => { + if (!selectedFile) { + showError("Error!", "Silakan pilih file terlebih dahulu"); + return; + } + + try { + setIsUploading(true); + + const formData = new FormData(); + formData.append('file', selectedFile); + + const response = await fetch('/api/keloladata/upload-mata-kuliah', { + method: 'POST', + body: formData, + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.message || 'Upload failed'); + } + + showSuccess("Berhasil!", `${result.successCount} mata kuliah berhasil diimport`); + onUploadSuccess(); + setIsDialogOpen(false); + setSelectedFile(null); + + // Reset file input + const fileInput = document.getElementById('file-upload') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } catch (error) { + console.error('Upload error:', error); + showError("Gagal!", error instanceof Error ? error.message : "Terjadi kesalahan saat upload"); + } finally { + setIsUploading(false); + } + }; + + // Download template + const downloadTemplate = () => { + // Create CSV template + const csvContent = "kode_mk,nama_mk,sks,semester,jenis_mk,kode_prasyarat\nINF101,Pengantar Informatika,3,1,Wajib,\nINF102,Algoritma dan Pemrograman,4,2,Wajib,INF101\nINF301,Basis Data Lanjut,3,5,Pilihan Wajib,\nINF401,Topik Khusus,2,7,Pilihan,\n"; + const blob = new Blob([csvContent], { type: 'text/csv' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'template_mata_kuliah.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + return ( + <> + + + + + + + + Import Data Mata Kuliah + + + +
      +
      + + +

      + Format yang didukung: .xlsx, .xls, .csv (Max: 10MB) +

      +
      + + {selectedFile && ( +
      +

      File terpilih:

      +

      {selectedFile.name}

      +

      + Ukuran: {(selectedFile.size / 1024 / 1024).toFixed(2)} MB +

      +
      + )} + +
      +

      Format File:

      +
      +

      • Kolom yang diperlukan: kode_mk, nama_mk, sks, semester, jenis_mk

      +

      • Kolom opsional: kode_prasyarat (kode mata kuliah prasyarat)

      +

      • Baris pertama harus berisi header kolom

      +

      • SKS dan semester harus berupa angka positif

      +

      • jenis_mk harus salah satu: Wajib, Pilihan Wajib, Pilihan

      +
      + + +
      +
      + + + + + + + +
      +
      + + ); +} diff --git a/components/datatable/upload-file-nilai-mahasiswa.tsx b/components/datatable/upload-file-nilai-mahasiswa.tsx new file mode 100644 index 0000000..3976f8c --- /dev/null +++ b/components/datatable/upload-file-nilai-mahasiswa.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose +} from "@/components/ui/dialog"; +import { Upload, FileSpreadsheet, Loader2 } from "lucide-react"; +import { useToast } from "@/components/ui/toast-provider"; + +interface UploadFileNilaiMahasiswaProps { + onUploadSuccess: () => void; +} + +export default function UploadFileNilaiMahasiswa({ onUploadSuccess }: UploadFileNilaiMahasiswaProps) { + const { showSuccess, showError } = useToast(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + // Handle file selection + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Validate file type + const validTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv' + ]; + + if (!validTypes.includes(file.type)) { + showError("Error!", "File harus berformat Excel (.xlsx, .xls) atau CSV (.csv)"); + return; + } + + setSelectedFile(file); + } + }; + + // Handle file upload + const handleUpload = async () => { + if (!selectedFile) { + showError("Error!", "Silakan pilih file terlebih dahulu"); + return; + } + + try { + setIsUploading(true); + + const formData = new FormData(); + formData.append('file', selectedFile); + + const response = await fetch('/api/keloladata/data-nilai-mahasiswa/upload', { + method: 'POST', + body: formData, + }); + + const result = await response.json(); + + if (!response.ok) { + let errorMessage = result.message || 'Upload failed'; + if (result.errors && result.errors.length > 0) { + errorMessage += '\n\nErrors:\n' + result.errors.slice(0, 5).join('\n'); + } + throw new Error(errorMessage); + } + + showSuccess("Berhasil!", `${result.successCount} nilai mahasiswa berhasil diimport`); + onUploadSuccess(); + setIsDialogOpen(false); + setSelectedFile(null); + + // Reset file input + const fileInput = document.getElementById('file-upload-nilai') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } catch (error) { + console.error('Upload error:', error); + showError("Gagal!", error instanceof Error ? error.message : "Terjadi kesalahan saat upload"); + } finally { + setIsUploading(false); + } + }; + + return ( + <> + + + + + + + + Import Data Nilai Mahasiswa + + + +
      +
      + + +

      + Format yang didukung: .xlsx, .xls, .csv (Max: 10MB) +

      +
      + + {selectedFile && ( +
      +

      File terpilih:

      +

      {selectedFile.name}

      +

      + Ukuran: {(selectedFile.size / 1024 / 1024).toFixed(2)} MB +

      +
      + )} + +
      +

      Format File:

      +
      +

      Header kolom: NIM, Kode MK, Semester, Nilai Huruf, Nilai Angka

      +

      NIM: Harus sudah terdaftar di sistem (contoh: D1041231002)

      +

      Kode MK: Harus sesuai persis dengan database (contoh: INF-55201-101)

      +

      Semester: Angka 1-8

      +

      Nilai Huruf: A, B+, B, C+, C, D+, D, E

      +

      Nilai Angka: 0-4 (bisa menggunakan koma: 3,5 atau titik: 3.5)

      +
      +
      +
      + + + + + + + +
      +
      + + ); +} diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index 1ebad74..deb4585 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { ThemeToggle } from '@/components/theme-toggle'; -import { Menu, ChevronDown, BarChart, Database, GraduationCap, BookOpen, Award, LogOut, User, Users } from 'lucide-react'; +import { Menu, ChevronDown, BarChart, Database, GraduationCap, BookOpen, Award, LogOut, User, Users, BookOpenText } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { @@ -220,6 +220,18 @@ const Navbar = () => { Dosen + + + + Mata Kuliah + + + + + + Nilai Mahasiswa + + )} @@ -339,6 +351,14 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => { Dosen + + + Mata Kuliah + + + + Nilai Mahasiswa +
      )} diff --git a/components/ui/filter-nama-dosen.tsx b/components/ui/filter-nama-dosen.tsx new file mode 100644 index 0000000..0bf19da --- /dev/null +++ b/components/ui/filter-nama-dosen.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface Props { + selectedDosen: string; + onDosenChange: (dosen: string) => void; +} + +export default function FilterNamaDosen({ selectedDosen, onDosenChange }: Props) { + const [dosenList, setDosenList] = useState([]); + + useEffect(() => { + const fetchDosenList = async () => { + try { + const response = await fetch('/api/dosen/list'); + if (!response.ok) { + throw new Error('Failed to fetch dosen list'); + } + const data = await response.json(); + setDosenList(data); + } catch (error) { + console.error('Error fetching dosen list:', error); + } + }; + + fetchDosenList(); + }, []); + + return ( +
      + Filter Dosen: + +
      + ); +} diff --git a/package-lock.json b/package-lock.json index e742fab..2b7ccb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@supabase/supabase-js": "^2.50.0", "@types/bcryptjs": "^2.4.6", "@types/jsonwebtoken": "^9.0.9", + "@types/xlsx": "^0.0.35", "apexcharts": "^4.5.0", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", @@ -3061,6 +3062,12 @@ "@types/node": "*" } }, + "node_modules/@types/xlsx": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.35.tgz", + "integrity": "sha512-s0x3DYHZzOkxtjqOk/Nv1ezGzpbN7I8WX+lzlV/nFfTDOv7x4d8ZwGHcnaiB8UCx89omPsftQhS5II3jeWePxQ==", + "license": "MIT" + }, "node_modules/@yr/monotone-cubic-spline": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", diff --git a/package.json b/package.json index 571b4be..865dd99 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@supabase/supabase-js": "^2.50.0", "@types/bcryptjs": "^2.4.6", "@types/jsonwebtoken": "^9.0.9", + "@types/xlsx": "^0.0.35", "apexcharts": "^4.5.0", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1",