diff --git a/app/api/mahasiswa/kategoriipk/route.ts b/app/api/mahasiswa/kategoriipk/route.ts new file mode 100644 index 0000000..ee855f3 --- /dev/null +++ b/app/api/mahasiswa/kategoriipk/route.ts @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface KategoriIPKData { + tahun_angkatan: number; + kategori_ipk: string; + jumlah: number; +} + +// Fungsi untuk mengkategorikan IPK +function getKategoriIPK(ipk: number): string { + if (ipk >= 3.00 && ipk <= 4.00) { + return 'Sangat Baik'; + } else if (ipk >= 2.50 && ipk < 3.00) { + return 'Baik'; + } else if (ipk >= 2.00 && ipk < 2.50) { + return 'Cukup'; + } else if (ipk < 2.00) { + return 'Kurang'; + } + return 'Tidak Terkategorikan'; +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + // Ambil data mahasiswa dengan IPK (tidak null) + let query = supabase + .from('mahasiswa') + .select('tahun_angkatan, ipk') + .not('ipk', 'is', null); + + // 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 kategori IPK data:', error); + return NextResponse.json( + { error: 'Failed to fetch kategori IPK data' }, + { status: 500 } + ); + } + + // Kelompokkan berdasarkan tahun_angkatan dan kategori_ipk + const groupedData = new Map(); + + data.forEach((item: any) => { + const tahun_angkatan = item.tahun_angkatan; + const ipk = Number(item.ipk); + + // Skip jika IPK tidak valid atau tahun_angkatan tidak ada + if (isNaN(ipk) || !tahun_angkatan) return; + + const kategori_ipk = getKategoriIPK(ipk); + // Gunakan separator yang lebih aman untuk key + const key = `${tahun_angkatan}||${kategori_ipk}`; + groupedData.set(key, (groupedData.get(key) || 0) + 1); + }); + + // Konversi ke format akhir + const results: KategoriIPKData[] = Array.from(groupedData.entries()).map(([key, jumlah]) => { + const [tahun_angkatan, kategori_ipk] = key.split('||'); + return { + tahun_angkatan: parseInt(tahun_angkatan), + kategori_ipk, + jumlah + }; + }); + + // Urutkan berdasarkan tahun_angkatan dan kategori_ipk + results.sort((a, b) => { + if (a.tahun_angkatan !== b.tahun_angkatan) { + return a.tahun_angkatan - b.tahun_angkatan; + } + // Urutkan kategori: Sangat Baik, Baik, Cukup, Kurang + const kategoriOrder = ['Sangat Baik', 'Baik', 'Cukup', 'Kurang']; + return kategoriOrder.indexOf(a.kategori_ipk) - kategoriOrder.indexOf(b.kategori_ipk); + }); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error fetching kategori IPK data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} + diff --git a/app/api/mahasiswa/terancamdo/route.ts b/app/api/mahasiswa/terancamdo/route.ts new file mode 100644 index 0000000..704a408 --- /dev/null +++ b/app/api/mahasiswa/terancamdo/route.ts @@ -0,0 +1,192 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +const MANDATORY = [ + 'UMG-55201-105', // Pendidikan Agama + 'UMG-55201-101', // Pendidikan Pancasila + 'UMG-55201-104', // Kewarganegaraan + 'UMG-55201-102', // Bahasa Indonesia +]; + +export async function GET() { + try { + // 1. Ambil data mahasiswa + const { data: mahasiswa, error: errMhs } = await supabase + .from('mahasiswa') + .select('id_mahasiswa, tahun_angkatan, ipk, semester'); + + if (errMhs) throw errMhs; + + // 2. Ambil semua nilai + info mata kuliah + const { data: nilai, error: errNilai } = await supabase + .from('nilai_mahasiswa') + .select('id_mahasiswa, nilai_huruf, nilai_angka, mata_kuliah(kode_mk, sks)'); + + if (errNilai) throw errNilai; + + // 3. Kelompokkan nilai per mahasiswa (mirip CTE total_sks & cekmk_wajib) + const nilaiMap = new Map(); + nilai?.forEach((n) => { + if (!nilaiMap.has(n.id_mahasiswa)) nilaiMap.set(n.id_mahasiswa, []); + nilaiMap.get(n.id_mahasiswa)!.push(n); + }); + + // Map jumlah DO per angkatan (hasil akhirnya) + const doPerAngkatan = new Map(); + + for (const m of mahasiswa ?? []) { + const tahun_angkatan = m.tahun_angkatan; // di DB bentuknya char(4) → string + if (!tahun_angkatan) continue; + + const list = nilaiMap.get(m.id_mahasiswa) ?? []; + const sem = m.semester ?? 0; + + // ipk bisa NULL → jangan dipaksa jadi 0 + const ipk = m.ipk === null ? null : Number(m.ipk); + + // ==== replikasi CTE total_sks ==== + // Jika tidak ada nilai, semua nilai akan NULL (sesuai LEFT JOIN di SQL) + // Di JavaScript, kita set ke null untuk menandakan tidak ada data + let sksTotal: number | null = null; // hanya nilai ≠ E + let sksD: number | null = null; // total SKS nilai D/D+ + let jumlahE: number | null = null; // jumlah matkul nilai E + let lulusTA1: number | null = null; // MAX(CASE WHEN ... THEN 1 ELSE 0 END) + let lulusTA2: number | null = null; // MAX(CASE WHEN ... THEN 1 ELSE 0 END) + let minWajib: number | null = null; // MIN(nilai_angka) FILTER (WHERE ...) + + if (list.length > 0) { + // Inisialisasi dengan 0 jika ada data + sksTotal = 0; + sksD = 0; + jumlahE = 0; + // lulusTA1 dan lulusTA2 tetap null, akan di-set ke 0 atau 1 jika ada nilai yang sesuai + + for (const n of list) { + const mk = n.mata_kuliah; + if (!mk) continue; + + const sks = mk.sks ?? 0; + const huruf = n.nilai_huruf; + const angka = n.nilai_angka; // bisa null → jangan dipaksa 0 + + // SKS total: hanya nilai != E + if (huruf !== 'E') { + sksTotal! += sks; + } + + // SKS D/D+ + if (huruf === 'D' || huruf === 'D+') { + sksD! += sks; + } + + // jumlah E + if (huruf === 'E') { + jumlahE! += 1; + } + + // nilai minimal mk wajib (MIN dengan FILTER) + if (MANDATORY.includes(mk.kode_mk) && angka !== null) { + if (minWajib === null || angka < minWajib) { + minWajib = angka; + } + } + + // TA1 & TA2: MAX(CASE WHEN mk.kode_mk = '...' AND nm.nilai_angka >= 2.0 THEN 1 ELSE 0 END) + // Artinya: + // - Jika ada setidaknya satu nilai >= 2.0, hasilnya 1 + // - Jika ada nilai TA1/TA2 tapi tidak ada yang >= 2.0, hasilnya 0 + // - Jika tidak ada nilai TA1/TA2 sama sekali, hasilnya NULL + if (mk.kode_mk === 'INF-55201-406') { + if (lulusTA1 === null) { + // Pertama kali menemukan nilai TA1, inisialisasi dengan 0 + lulusTA1 = 0; + } + if (angka !== null && angka >= 2.0) { + lulusTA1 = 1; + } + } + if (mk.kode_mk === 'INF-55201-407') { + if (lulusTA2 === null) { + // Pertama kali menemukan nilai TA2, inisialisasi dengan 0 + lulusTA2 = 0; + } + if (angka !== null && angka >= 2.0) { + lulusTA2 = 1; + } + } + } + } + + // ==== replikasi CTE evaluasi (CASE ... WHEN ... THEN 1 ELSE 0) ==== + // Sesuai dengan query SQL: CASE WHEN ... THEN 1 ELSE 0 END + // Di SQL, jika nilai NULL, kondisi akan menghasilkan NULL (dianggap FALSE dalam CASE WHEN) + let isDO = 0; + + // Evaluasi 4 semester: semester BETWEEN 4 AND 7 AND (sks_total < 40 OR ipk <= 2.50) + // Jika sksTotal NULL (tidak ada nilai), kondisi sksTotal < 40 akan NULL (FALSE) + if ( + sem >= 4 && + sem <= 7 && + sksTotal !== null && // Pastikan ada data nilai + ( + sksTotal < 40 || + (ipk !== null && ipk <= 2.50) + ) + ) { + isDO = 1; + } + // Evaluasi 8 semester: semester BETWEEN 8 AND 13 AND (sks_total < 80 OR ipk <= 2.50) + else if ( + sem >= 8 && + sem <= 13 && + sksTotal !== null && // Pastikan ada data nilai + ( + sksTotal < 80 || + (ipk !== null && ipk <= 2.50) + ) + ) { + isDO = 1; + } + // Evaluasi akhir masa studi: semester = 14 AND (sks_total < 144 OR ipk <= 2.00 OR jumlah_e > 0 OR sks_d > 14 OR min_wajib < 2.00 OR lulus_ta1 = 0 OR lulus_ta2 = 0) + // Di SQL: lulus_ta1 = 0 akan TRUE jika lulus_ta1 adalah 0, FALSE jika NULL atau 1 + else if ( + sem === 14 && + sksTotal !== null && // Pastikan ada data nilai + ( + sksTotal < 144 || + (ipk !== null && ipk <= 2.00) || + (jumlahE !== null && jumlahE > 0) || + (sksD !== null && sksD > 14) || + (minWajib !== null && minWajib < 2.00) || + lulusTA1 === 0 || // TRUE jika 0, FALSE jika NULL atau 1 + lulusTA2 === 0 // TRUE jika 0, FALSE jika NULL atau 1 + ) + ) { + isDO = 1; + } + + if (isDO === 1) { + doPerAngkatan.set( + tahun_angkatan, + (doPerAngkatan.get(tahun_angkatan) ?? 0) + 1 + ); + } + } + + // Susun output sama seperti query SQL + // HAVING COUNT(*) FILTER (WHERE is_do = 1) > 0 + // Artinya hanya menampilkan tahun_angkatan yang memiliki setidaknya 1 mahasiswa DO + const output = [...doPerAngkatan.entries()] + .filter(([_, jumlah]) => jumlah > 0) // Filter sesuai HAVING clause + .map(([tahun_angkatan, jumlah_mahasiswa_do]) => ({ + tahun_angkatan, + jumlah_mahasiswa_do, + })) + .sort((a, b) => Number(a.tahun_angkatan) - Number(b.tahun_angkatan)); + + return NextResponse.json(output); + } catch (err) { + console.error('❌ Error Evaluasi DO:', err); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/api/tabeldetail/kategori-ipk/route.ts b/app/api/tabeldetail/kategori-ipk/route.ts new file mode 100644 index 0000000..150ae78 --- /dev/null +++ b/app/api/tabeldetail/kategori-ipk/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaKategoriIPK { + nim: string; + nama: string; + tahun_angkatan: number; + ipk: number; +} + +// Fungsi untuk mengkategorikan IPK +function getKategoriIPK(ipk: number): string { + if (ipk >= 3.00 && ipk <= 4.00) { + return 'Sangat Baik'; + } else if (ipk >= 2.50 && ipk < 3.00) { + return 'Baik'; + } else if (ipk >= 2.00 && ipk < 2.50) { + return 'Cukup'; + } else if (ipk < 2.00) { + return 'Kurang'; + } + return 'Tidak Terkategorikan'; +} + +// GET - Ambil data mahasiswa dengan IPK dan filter tahun angkatan serta kategori IPK untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const kategoriIPK = searchParams.get('kategori_ipk'); + + let query = supabase + .from('mahasiswa') + .select('nim, nama, ipk, tahun_angkatan') + .not('ipk', 'is', null) // Hanya ambil mahasiswa yang memiliki IPK + .order('ipk', { ascending: false }); // Urutkan berdasarkan IPK tertinggi + + // 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 } + ); + } + + // Filter berdasarkan kategori IPK jika diberikan + let filteredData = (data || []) as any[]; + + if (kategoriIPK && kategoriIPK !== 'all') { + filteredData = filteredData.filter(item => { + const ipk = Number(item.ipk); + if (isNaN(ipk)) return false; + + const kategori = getKategoriIPK(ipk); + return kategori === kategoriIPK; + }); + } + + // Transform data untuk memastikan format yang benar + const transformedData: MahasiswaKategoriIPK[] = filteredData.map((item: any) => ({ + nim: item.nim, + nama: item.nama, + tahun_angkatan: Number(item.tahun_angkatan), + ipk: Number(item.ipk) + })); + + 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/kategori-ipk API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} + diff --git a/app/api/tabeldetail/terancam-do/route.ts b/app/api/tabeldetail/terancam-do/route.ts new file mode 100644 index 0000000..d0d8d32 --- /dev/null +++ b/app/api/tabeldetail/terancam-do/route.ts @@ -0,0 +1,241 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +const MANDATORY = [ + 'UMG-55201-105', // Pendidikan Agama + 'UMG-55201-101', // Pendidikan Pancasila + 'UMG-55201-104', // Kewarganegaraan + 'UMG-55201-102', // Bahasa Indonesia +]; + +interface MahasiswaTerancamDO { + nim: string; + nama: string; + tahun_angkatan: number; + ipk: number | null; + semester: number; + sks_total: number | null; + alasan_do: string; +} + +// GET - Ambil data detail mahasiswa terancam DO dengan alasan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + // 1. Ambil data mahasiswa + let mahasiswaQuery = supabase + .from('mahasiswa') + .select('id_mahasiswa, nim, nama, tahun_angkatan, ipk, semester'); + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + mahasiswaQuery = mahasiswaQuery.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data: mahasiswa, error: errMhs } = await mahasiswaQuery; + + if (errMhs) throw errMhs; + + // 2. Ambil semua nilai + info mata kuliah + const { data: nilai, error: errNilai } = await supabase + .from('nilai_mahasiswa') + .select('id_mahasiswa, nilai_huruf, nilai_angka, mata_kuliah(kode_mk, sks)'); + + if (errNilai) throw errNilai; + + // 3. Kelompokkan nilai per mahasiswa (mirip CTE total_sks & cekmk_wajib) + const nilaiMap = new Map(); + nilai?.forEach((n) => { + if (!nilaiMap.has(n.id_mahasiswa)) nilaiMap.set(n.id_mahasiswa, []); + nilaiMap.get(n.id_mahasiswa)!.push(n); + }); + + // Array untuk menyimpan hasil akhir + const hasil: MahasiswaTerancamDO[] = []; + + for (const m of mahasiswa ?? []) { + const tahun_angkatan = m.tahun_angkatan; + if (!tahun_angkatan) continue; + + const list = nilaiMap.get(m.id_mahasiswa) ?? []; + const sem = m.semester ?? 0; + + // ipk bisa NULL → jangan dipaksa jadi 0 + const ipk = m.ipk === null ? null : Number(m.ipk); + + // ==== replikasi CTE total_sks ==== + let sksTotal: number | null = null; + let sksD: number | null = null; + let jumlahE: number | null = null; + let lulusTA1: number | null = null; + let lulusTA2: number | null = null; + let minWajib: number | null = null; + + if (list.length > 0) { + sksTotal = 0; + sksD = 0; + jumlahE = 0; + + for (const n of list) { + const mk = n.mata_kuliah; + if (!mk) continue; + + const sks = mk.sks ?? 0; + const huruf = n.nilai_huruf; + const angka = n.nilai_angka; + + // SKS total: hanya nilai != E + if (huruf !== 'E') { + sksTotal! += sks; + } + + // SKS D/D+ + if (huruf === 'D' || huruf === 'D+') { + sksD! += sks; + } + + // jumlah E + if (huruf === 'E') { + jumlahE! += 1; + } + + // nilai minimal mk wajib (MIN dengan FILTER) + if (MANDATORY.includes(mk.kode_mk) && angka !== null) { + if (minWajib === null || angka < minWajib) { + minWajib = angka; + } + } + + // TA1 & TA2 + if (mk.kode_mk === 'INF-55201-406') { + if (lulusTA1 === null) { + lulusTA1 = 0; + } + if (angka !== null && angka >= 2.0) { + lulusTA1 = 1; + } + } + if (mk.kode_mk === 'INF-55201-407') { + if (lulusTA2 === null) { + lulusTA2 = 0; + } + if (angka !== null && angka >= 2.0) { + lulusTA2 = 1; + } + } + } + } + + // ==== Evaluasi kondisi DO dan kumpulkan alasan ==== + const alasanList: string[] = []; + let isDO = false; + + // Evaluasi 4 semester: semester BETWEEN 4 AND 7 AND (sks_total < 40 OR ipk <= 2.50) + if ( + sem >= 4 && + sem <= 7 && + sksTotal !== null && + ( + sksTotal < 40 || + (ipk !== null && ipk <= 2.50) + ) + ) { + isDO = true; + if (sksTotal < 40) { + alasanList.push('SKS kurang dari 40 pada evaluasi semester 4'); + } + if (ipk !== null && ipk <= 2.50) { + alasanList.push('IPK kurang dari 2.50 pada evaluasi semester 4'); + } + } + // Evaluasi 8 semester: semester BETWEEN 8 AND 13 AND (sks_total < 80 OR ipk <= 2.50) + else if ( + sem >= 8 && + sem <= 13 && + sksTotal !== null && + ( + sksTotal < 80 || + (ipk !== null && ipk <= 2.50) + ) + ) { + isDO = true; + if (sksTotal < 80) { + alasanList.push('SKS kurang dari 80 pada evaluasi semester 8'); + } + if (ipk !== null && ipk <= 2.50) { + alasanList.push('IPK kurang dari 2.50 pada evaluasi semester 8'); + } + } + // Evaluasi akhir masa studi: semester = 14 + else if ( + sem === 14 && + sksTotal !== null && + ( + sksTotal < 144 || + (ipk !== null && ipk <= 2.00) || + (jumlahE !== null && jumlahE > 0) || + (sksD !== null && sksD > 14) || + (minWajib !== null && minWajib < 2.00) || + lulusTA1 === 0 || + lulusTA2 === 0 + ) + ) { + isDO = true; + if (sksTotal < 144) { + alasanList.push('Belum mencapai 144 SKS pada akhir masa studi'); + } + if (ipk !== null && ipk <= 2.00) { + alasanList.push('IPK di bawah 2.00 pada akhir masa studi'); + } + if (jumlahE !== null && jumlahE > 0) { + alasanList.push('Memiliki nilai E pada akhir masa studi'); + } + if (sksD !== null && sksD > 14) { + alasanList.push('Total SKS dari nilai D lebih dari 14 SKS'); + } + if (minWajib !== null && minWajib < 2.00) { + alasanList.push('Nilai minimal mata kuliah wajib di bawah C'); + } + if (lulusTA1 === 0) { + alasanList.push('Belum lulus Tugas Akhir 1 (INF-55201-406)'); + } + if (lulusTA2 === 0) { + alasanList.push('Belum lulus Tugas Akhir 2 (INF-55201-407)'); + } + } + + // Hanya tambahkan ke hasil jika terancam DO + if (isDO) { + hasil.push({ + nim: m.nim, + nama: m.nama, + tahun_angkatan: Number(tahun_angkatan), + ipk: ipk, + semester: sem, + sks_total: sksTotal, + alasan_do: alasanList.join('; '), + }); + } + } + + // Urutkan berdasarkan tahun_angkatan ASC + hasil.sort((a, b) => a.tahun_angkatan - b.tahun_angkatan); + + return NextResponse.json(hasil, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/terancam-do API:', error); + return NextResponse.json( + { message: 'Internal Server Error', error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} + diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 1c6bc18..9464670 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -15,6 +15,7 @@ import KelompokKeahlianLulusTepatPieChart from "@/components/chartsDashboard/kkd import KelompokKeahlianPieChartPerAngkatan from "@/components/chartsDashboard/kkdashboardpiechartperangkatan"; import StatusMahasiswaPieChartPerangkatan from "@/components/chartsDashboard/StatusMahasiswaPieChartPerangkatan"; import MasaStudiLulusChart from "@/components/chartsDashboard/masastudiluluschart"; +import TerancamDOChart from "@/components/chartsDashboard/TerancamDOChart"; import NamaBeasiswaChart from "@/components/chartsDashboard/NamaBeasiswaDashChart"; import NamaBeasiswaDashPieChartPerangkatan from "@/components/chartsDashboard/NamaBeasiswaDashPieChartPerangkatan"; import TingkatPrestasiChart from "@/components/chartsDashboard/TingkatPrestasiDashChart"; @@ -23,6 +24,8 @@ import TingkatPrestasiPieChartDash from "@/components/chartsDashboard/TingkatPre import LulusTepatWaktuChart from "@/components/charts/LulusTepatWaktuChart"; import BimbinganDosenChart from "@/components/charts/BimbinganDosenChart"; import BimbinganDosenPerAngkatanChart from "@/components/charts/BimbinganDosenPerAngkatanChart"; +import DistribusiIPKChart from "@/components/chartsDashboard/DistribusiIPKChart"; +import DistribusiIPKChartPerangkatan from "@/components/chartsDashboard/DistribusiIPKChartPerangkatan"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { @@ -135,8 +138,10 @@ export default function TotalMahasiswaPage() { // Navigation menu items for per year data const perYearNavItems = [ { id: 'overview-year', label: 'Jumlah & Status per Angkatan' }, + { id: 'status-year', label: 'Jenis Pendaftaran & Kelompok Keahlian' }, { id: 'achievement-year', label: 'Beasiswa & Prestasi per Angkatan' }, + { id: 'academic-year', label: 'Distribusi IPK per Angkatan' }, { id: 'demographics-year', label: 'Asal Kabupaten per Angkatan' }, { id: 'bimbingan-dosen-year', label: 'Bimbingan Dosen' } ]; @@ -213,6 +218,8 @@ export default function TotalMahasiswaPage() {
+ +
{/* Expertise & Achievement Section */} @@ -260,6 +267,11 @@ export default function TotalMahasiswaPage() { + {/* Distribusi IPK Section */} +
+ +
+ {/* Demographics Section */}
diff --git a/app/detail/kategori-ipk/page.tsx b/app/detail/kategori-ipk/page.tsx new file mode 100644 index 0000000..821ef1d --- /dev/null +++ b/app/detail/kategori-ipk/page.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { useState } from "react"; +import DistribusiIPKChart from "@/components/chartsDashboard/DistribusiIPKChart"; +import DistribusiIPKChartPerangkatan from "@/components/chartsDashboard/DistribusiIPKChartPerangkatan"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelKategoriIPKMahasiswa from "@/components/chartstable/tabelkategoriipkmahasiswa"; + +export default function KategoriIPKDetailPage() { + const [selectedYear, setSelectedYear] = useState("all"); + + return ( +
+
+ {/* Filter Section */} + + + {/* Chart Section */} +
+ {/* Chart untuk semua data atau chart per angkatan ketika tahun tertentu dipilih */} + {selectedYear === "all" ? ( + + ) : ( + + )} +
+ + {/* Tabel Section */} + + + {/* Information Section */} +
+

+ Informasi Visualisasi +

+
+
+

+ Grafik Distribusi IPK Mahasiswa +

+
    +
  • • Menampilkan distribusi mahasiswa berdasarkan kategori IPK per tahun angkatan
  • +
  • • IPK dikategorikan menjadi empat kelompok berdasarkan skala penilaian:
  • +
  • - 4.00 - 3.00 Sangat Baik: Mahasiswa dengan IPK sangat baik
  • +
  • - 2.99 - 2.50 Baik: Mahasiswa dengan IPK baik
  • +
  • - 2.49 - 2.00 Cukup: Mahasiswa dengan IPK cukup
  • +
  • - < 2.00 Kurang: Mahasiswa dengan IPK kurang
  • +
  • • Grafik stacked bar chart horizontal yang menunjukkan persentase setiap kategori IPK per tahun angkatan
  • +
  • • Data dapat di-download dan dianalisis untuk monitoring kualitas akademik mahasiswa
  • +
  • • Filter berdasarkan tahun angkatan tersedia untuk analisis per angkatan
  • +
+
+
+
+
+
+ ); +} + diff --git a/app/detail/terancam-do/page.tsx b/app/detail/terancam-do/page.tsx new file mode 100644 index 0000000..0aa565d --- /dev/null +++ b/app/detail/terancam-do/page.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useState } from "react"; +import TerancamDOChart from "@/components/chartsDashboard/TerancamDOChart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelNamaTerancamDO from "@/components/chartstable/tabelnamaterancamdo"; + +export default function TerancamDODetailPage() { + const [selectedYear, setSelectedYear] = useState("all"); + + return ( +
+
+ {/* Filter Section */} + + + {/* Chart Section */} +
+ +
+ + {/* Tabel Section */} + + + {/* Information Section */} +
+

+ Informasi Visualisasi +

+
+
+

+ Grafik Mahasiswa Terancam Drop Out +

+
    +
  • • Menampilkan jumlah mahasiswa yang terancam drop out (DO) per tahun angkatan
  • +
  • • Evaluasi dilakukan berdasarkan pedoman akademik UNTAN tahun 2023/2024
  • +
  • • Kriteria evaluasi terdiri dari tiga tahap:
  • +
  • - Evaluasi 4 semester: SKS minimal 40 dan IPK > 2.50
  • +
  • - Evaluasi 8 semester: SKS minimal 80 dan IPK > 2.50
  • +
  • - Evaluasi akhir masa studi: SKS minimal 144, IPK > 2.00, tidak ada nilai E, nilai D maksimal 10%, nilai mata kuliah wajib minimal C, dan lulus tugas akhir
  • +
  • • Grafik batang vertikal yang menunjukkan jumlah mahasiswa terancam DO per tahun angkatan
  • +
  • • Data dapat di-download dan dianalisis untuk monitoring akademik
  • +
+
+
+
+
+
+ ); +} diff --git a/components/chartsDashboard/DistribusiIPKChart.tsx b/components/chartsDashboard/DistribusiIPKChart.tsx new file mode 100644 index 0000000..4514735 --- /dev/null +++ b/components/chartsDashboard/DistribusiIPKChart.tsx @@ -0,0 +1,359 @@ +'use client'; + +import { useEffect, useState, useMemo } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { ExternalLink } from "lucide-react"; +import Link from "next/link"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface KategoriIPKData { + tahun_angkatan: number; + kategori_ipk: string; + jumlah: number; +} + +interface DistribusiIPKChartProps { + selectedYear?: string; + height?: string; + showDetailButton?: boolean; +} + +export default function DistribusiIPKChart({ + selectedYear = 'all', + height = "h-[300px] sm:h-[300px] md:h-[300px]", + showDetailButton = true +}: DistribusiIPKChartProps) { + const { theme } = useTheme(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([]); + + // Get unique tahun angkatan and sort them + const tahunAngkatan = useMemo(() => { + return [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => a - b); + }, [data]); + + const chartOptions: ApexOptions = useMemo(() => ({ + 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: tahunAngkatan, + 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 + }, + yaxis: { + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + fill: { + opacity: 1, + }, + legend: { + position: 'top', + fontSize: '11px', + markers: { + size: 8, + }, + itemMargin: { + horizontal: 12, + vertical: 8, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000', + }, + formatter: function(seriesName: string) { + return seriesName; + } + }, + colors: ['#008FFB', '#00E396', '#FEB019', '#EF4444'], // Sangat Baik, Baik, Cukup, Kurang + 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 kategoriLabels = [ + '4.00 - 3.00 Sangat Baik', + '2.99 - 2.50 Baik', + '2.49 - 2.00 Cukup', + '< 2.00 Kurang' + ]; + const kategoriColors = ['#008FFB', '#00E396', '#FEB019', '#EF4444']; + + let tooltipContent = ` +
+
Angkatan ${tahun}
`; + + // Tambahkan setiap kategori + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + if (value > 0) { + const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0'; + tooltipContent += ` +
+
+ ${kategoriLabels[index]} + ${value} (${percentage}%) +
`; + } + }); + + // Tambahkan total + tooltipContent += ` +
+
+ Total + ${total} +
+
+ `; + + return tooltipContent; + } + } + }), [theme, tahunAngkatan]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = selectedYear === 'all' + ? '/api/mahasiswa/kategoriipk' + : `/api/mahasiswa/kategoriipk?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + // Process data to create series + const tahunAngkatan = [...new Set(result.map(item => item.tahun_angkatan))].sort((a, b) => a - b); + const kategori = ['Sangat Baik', 'Baik', 'Cukup', 'Kurang']; + // Label lengkap dengan angka IPK untuk ditampilkan di legend + const kategoriLabels = [ + '4.00 - 3.00 Sangat Baik', + '2.99 - 2.50 Baik', + '2.49 - 2.00 Cukup', + '< 2.00 Kurang' + ]; + + // Buat series data dengan label lengkap (angka IPK + kategori) + const seriesData = kategori.map((kat, index) => ({ + name: kategoriLabels[index], // Label ini akan ditampilkan di legend + data: tahunAngkatan.map(tahun => { + const item = result.find(d => d.tahun_angkatan === tahun && d.kategori_ipk === kat); + return item ? item.jumlah : 0; + }), + })); + + setSeries(seriesData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + if (loading) { + return ( + + + + Memuat data distribusi IPK... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Distribusi IPK + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +

+ Tidak ada data yang tersedia untuk periode ini. +

+
+
+ ); + } + + return ( + + +
+ + Distribusi IPK + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + {showDetailButton && ( + + + + )} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} + diff --git a/components/chartsDashboard/DistribusiIPKChartPerangkatan.tsx b/components/chartsDashboard/DistribusiIPKChartPerangkatan.tsx new file mode 100644 index 0000000..f6f5eca --- /dev/null +++ b/components/chartsDashboard/DistribusiIPKChartPerangkatan.tsx @@ -0,0 +1,222 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface KategoriIPKPerAngkatanData { + kategori_ipk: string; + jumlah: number; +} + +interface Props { + selectedYear: string; +} + +export default function DistribusiIPKChartPerangkatan({ selectedYear }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Label lengkap dengan angka IPK untuk ditampilkan di legend + const kategoriLabelsMap: { [key: string]: string } = { + 'Sangat Baik': '4.00 - 3.00 Sangat Baik', + 'Baik': '2.99 - 2.50 Baik', + 'Cukup': '2.49 - 2.00 Cukup', + 'Kurang': '< 2.00 Kurang' + }; + + // Urutan kategori untuk sorting + const kategoriOrder = ['Sangat Baik', 'Baik', 'Cukup', 'Kurang']; + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const response = await fetch( + `/api/mahasiswa/kategoriipk?tahun_angkatan=${selectedYear}` + ); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = await response.json(); + + // Filter data for selected year and group by kategori_ipk + const yearData = result.filter((item: any) => + item.tahun_angkatan.toString() === selectedYear + ); + + // Group by kategori_ipk and sum jumlah + const groupedData = yearData.reduce((acc: { [key: string]: number }, item: any) => { + const kategori = item.kategori_ipk || 'Tidak Diketahui'; + acc[kategori] = (acc[kategori] || 0) + item.jumlah; + return acc; + }, {}); + + // Convert to array format + const chartData = Object.entries(groupedData).map(([kategori_ipk, jumlah]) => ({ + kategori_ipk, + jumlah: jumlah as number + })); + + // Sort by kategori order + const sortedData = chartData.sort((a, b) => { + const indexA = kategoriOrder.indexOf(a.kategori_ipk); + const indexB = kategoriOrder.indexOf(b.kategori_ipk); + return indexA - indexB; + }); + + setData(sortedData); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [selectedYear]); + + // Prepare data for pie chart + const series = data.map(item => item.jumlah); + const labels = data.map(item => kategoriLabelsMap[item.kategori_ipk] || item.kategori_ipk); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels: labels, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + } + }, + legend: { + position: 'bottom', + fontSize: '11px', + markers: { + size: 8, + }, + itemMargin: { + horizontal: 10, + vertical: 5, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + }, + formatter: function(seriesName: string) { + return seriesName; + } + }, + colors: [ + '#008FFB', // Sangat Baik - Blue + '#00E396', // Baik - Green + '#FEB019', // Cukup - Orange + '#EF4444', // Kurang - Red + ], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + ' mahasiswa'; + } + } + }, + plotOptions: { + pie: { + donut: { + size: '0%', + }, + offsetY: 0, + }, + }, + states: { + hover: { + filter: { + type: 'darken', + }, + }, + }, + }; + + if (loading) { + return ( + + + + Memuat data distribusi IPK... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Distribusi IPK + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +

+ Tidak ada data yang tersedia untuk periode ini. +

+
+
+ ); + } + + return ( + + + + Distribusi IPK + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} + diff --git a/components/chartsDashboard/TerancamDOChart.tsx b/components/chartsDashboard/TerancamDOChart.tsx new file mode 100644 index 0000000..22ec065 --- /dev/null +++ b/components/chartsDashboard/TerancamDOChart.tsx @@ -0,0 +1,255 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { ExternalLink } from 'lucide-react'; +import Link from 'next/link'; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TerancamDOResponse { + tahun_angkatan: number; + jumlah_mahasiswa_do: number; +} + +interface TerancamDOChartProps { + selectedYear?: string; + height?: string; + showDetailButton?: boolean; +} + +export default function TerancamDOChart({ + selectedYear = 'all', + height = 'h-[300px] sm:h-[320px]', + showDetailButton = true, +}: TerancamDOChartProps) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = + selectedYear === 'all' + ? '/api/mahasiswa/terancamdo' + : `/api/mahasiswa/terancamdo?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to fetch data: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + const sorted = [...result].sort( + (a, b) => a.tahun_angkatan - b.tahun_angkatan, + ); + + setData(sorted); + } catch (err) { + console.error('Error fetching terancam DO data:', err); + setError( + err instanceof Error ? err.message : 'Terjadi kesalahan saat memuat data', + ); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + const categories = useMemo( + () => data.map((item) => item.tahun_angkatan), + [data], + ); + + const series = useMemo( + () => [ + { + name: 'Mahasiswa Terancam DO', + data: data.map((item) => item.jumlah_mahasiswa_do), + }, + ], + [data], + ); + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + toolbar: { + show: true, + }, + background: 'transparent', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 2, + }, + }, + dataLabels: { + enabled: true, + formatter: (val: number) => val.toString(), + style: { + colors: [theme === 'dark' ? '#f9fafb' : '#1f2937'], + }, + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'], + }, + xaxis: { + categories, + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#f9fafb' : '#1f2937', + }, + }, + labels: { + style: { + colors: theme === 'dark' ? '#e5e7eb' : '#4b5563', + }, + }, + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#f9fafb' : '#1f2937', + }, + }, + labels: { + style: { + colors: theme === 'dark' ? '#e5e7eb' : '#4b5563', + }, + }, + min: 0, + forceNiceScale: true, + }, + legend: { + show: false, + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#e5e7eb', + strokeDashArray: 4, + padding: { + top: 20, + right: 20, + left: 10, + }, + }, + colors: ['#ef4444'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: (val: number) => `${val} mahasiswa`, + }, + }, + }; + + if (loading) { + return ( + + + + Memuat data terancam DO... + + + + ); + } + + if (error) { + return ( + + + + Gagal memuat data + + + +

+ {error} +

+
+
+ ); + } + + if (!data.length) { + return ( + + + + Mahasiswa Terancam DO + + + +

+ Tidak ada mahasiswa yang teridentifikasi terancam DO pada periode ini. +

+
+
+ ); + } + + return ( + + +
+ + Mahasiswa Terancam Drop Out + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + {showDetailButton && ( + + + + )} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} + diff --git a/components/chartstable/tabelkategoriipkmahasiswa.tsx b/components/chartstable/tabelkategoriipkmahasiswa.tsx new file mode 100644 index 0000000..f304003 --- /dev/null +++ b/components/chartstable/tabelkategoriipkmahasiswa.tsx @@ -0,0 +1,451 @@ +'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, Medal, Award } from "lucide-react"; + +interface MahasiswaKategoriIPK { + nim: string; + nama: string; + tahun_angkatan: number; + ipk: number; +} + +interface TabelKategoriIPKMahasiswaProps { + selectedYear: string; +} + +export default function TabelKategoriIPKMahasiswa({ selectedYear }: TabelKategoriIPKMahasiswaProps) { + const [mahasiswaData, setMahasiswaData] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedKategori, setSelectedKategori] = useState("all"); + + // 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); + + // Fetch data dari API dengan filter tahun angkatan dan kategori IPK + const urlParams = new URLSearchParams(); + if (selectedYear !== 'all') { + urlParams.append('tahun_angkatan', selectedYear); + } + if (selectedKategori !== 'all') { + urlParams.append('kategori_ipk', selectedKategori); + } + + const url = urlParams.toString() + ? `/api/tabeldetail/kategori-ipk?${urlParams.toString()}` + : '/api/tabeldetail/kategori-ipk'; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa kategori IPK data'); + } + + const data = await response.json(); + setMahasiswaData(data); + setFilteredData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + setCurrentPage(1); // Reset to first page when filter changes + }, [selectedYear, selectedKategori]); + + // Update paginated data when filtered data or pagination settings change + useEffect(() => { + paginateData(); + }, [filteredData, currentPage, pageSize]); + + // 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 + }; + + // Handle kategori change + const handleKategoriChange = (kategori: string) => { + setSelectedKategori(kategori); + }; + + // Fungsi untuk mendapatkan icon berdasarkan ranking + const getRankingIcon = (index: number) => { + if (index === 0) return ; + if (index === 1) return ; + if (index === 2) return ; + return null; + }; + + // Fungsi untuk mendapatkan kategori IPK dari nilai IPK + const getKategoriIPKFromValue = (ipk: number): string => { + if (ipk >= 3.00 && ipk <= 4.00) { + return 'Sangat Baik'; + } else if (ipk >= 2.50 && ipk < 3.00) { + return 'Baik'; + } else if (ipk >= 2.00 && ipk < 2.50) { + return 'Cukup'; + } else if (ipk < 2.00) { + return 'Kurang'; + } + return 'Tidak Terkategorikan'; + }; + + // Hitung statistik IPK berdasarkan filtered data + const ipkStats = { + highest: filteredData.length > 0 ? Math.max(...filteredData.map(m => m.ipk)) : 0, + lowest: filteredData.length > 0 ? Math.min(...filteredData.map(m => m.ipk)) : 0, + average: filteredData.length > 0 ? + filteredData.reduce((sum, m) => sum + m.ipk, 0) / filteredData.length : 0, + total: filteredData.length + }; + + // Hitung jumlah per kategori + const kategoriStats = filteredData.reduce((acc, mahasiswa) => { + const kategori = getKategoriIPKFromValue(mahasiswa.ipk); + acc[kategori] = (acc[kategori] || 0) + 1; + return acc; + }, {} as Record); + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + Tabel Kategori IPK Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Filter Kategori IPK */} +
+ Filter Kategori IPK: + +
+ {/* Tampilkan ringkasan statistik IPK */} +
+
+
IPK Tertinggi
+
{ipkStats.highest > 0 ? ipkStats.highest.toFixed(2) : '-'}
+
+
+
IPK Terendah
+
{ipkStats.lowest > 0 ? ipkStats.lowest.toFixed(2) : '-'}
+
+
+
Rata-rata IPK
+
{ipkStats.average > 0 ? ipkStats.average.toFixed(2) : '-'}
+
+
+
Total Mahasiswa
+
{ipkStats.total}
+
+
+ {/* Tampilkan ringkasan per kategori */} + {selectedKategori === 'all' && Object.keys(kategoriStats).length > 0 && ( +
+ {Object.entries(kategoriStats) + .sort(([a], [b]) => { + const order = ['Sangat Baik', 'Baik', 'Cukup', 'Kurang']; + return order.indexOf(a) - order.indexOf(b); + }) + .map(([kategori, count]) => ( + + {kategori}: {count} + + ))} +
+ )} +
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + Ranking + NIM + Nama Mahasiswa + Tahun Angkatan + IPK + Kategori IPK + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => { + const globalIndex = (currentPage - 1) * pageSize + index; + const kategoriIPK = getKategoriIPKFromValue(mahasiswa.ipk); + return ( + + +
+ {getRankingIcon(globalIndex)} + {globalIndex + 1} +
+
+ + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.ipk.toFixed(2)} + + + + {kategoriIPK} + + +
+ ); + }) + )} +
+
+
+ + {/* 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"} + /> + + + +
+ )} +
+
+ ); + + // Calculate the range of entries being displayed + function 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 }; + } + + // 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/tabelnamaterancamdo.tsx b/components/chartstable/tabelnamaterancamdo.tsx new file mode 100644 index 0000000..04d39f9 --- /dev/null +++ b/components/chartstable/tabelnamaterancamdo.tsx @@ -0,0 +1,379 @@ +'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, AlertTriangle } from "lucide-react"; + +interface MahasiswaTerancamDO { + nim: string; + nama: string; + tahun_angkatan: number; + ipk: number | null; + semester: number; + sks_total: number | null; + alasan_do: string; +} + +interface TabelNamaTerancamDOProps { + selectedYear: string; +} + +export default function TabelNamaTerancamDO({ selectedYear }: TabelNamaTerancamDOProps) { + 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/terancam-do' + : `/api/tabeldetail/terancam-do?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa terancam DO 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 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 semester + const semesterStats = mahasiswaData.reduce((acc, mahasiswa) => { + const sem = mahasiswa.semester; + acc[sem] = (acc[sem] || 0) + 1; + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_tahun_angkatan: Object.keys(tahunAngkatanStats).length, + total_semester: Object.keys(semesterStats).length + }; + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Mahasiswa Terancam DO {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
+
+
Total Mahasiswa Terancam DO
+
{stats.total}
+
+
+ {/* Tampilkan ringkasan per tahun angkatan */} +
+ {Object.entries(tahunAngkatanStats) + .sort(([a], [b]) => Number(a) - Number(b)) // Urutkan berdasarkan tahun angkatan + .map(([tahun, count]) => ( + + Angkatan {tahun}: {count} mahasiswa + + ))} +
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + IPK + Semester + SKS Total + Alasan DO + + + + {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.ipk !== null ? mahasiswa.ipk.toFixed(2) : '-'} + + + {mahasiswa.semester} + + + {mahasiswa.sks_total !== null ? mahasiswa.sks_total : '-'} + + + {mahasiswa.alasan_do || '-'} + + + )) + )} + +
+
+ + {/* 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; + } +}