From 700a153b86592c2e3ade1a481964bc26e0b6b97e Mon Sep 17 00:00:00 2001 From: Randa Firman Putra Date: Thu, 4 Dec 2025 22:15:37 +0700 Subject: [PATCH] again n again --- app/api/mahasiswa/mkbelumdiambil/route.ts | 156 ++++++ app/api/mahasiswa/terancamdo/route.ts | 16 +- app/api/tabeldetail/mk-belum-diambil/route.ts | 154 ++++++ app/api/tabeldetail/terancam-do/route.ts | 36 +- app/dashboard/page.tsx | 14 +- app/detail/mk-belum-diambil/page.tsx | 72 +++ app/detail/terancam-do/page.tsx | 8 +- components/charts/BimbinganDosenChart.tsx | 2 +- .../charts/BimbinganDosenPerAngkatanChart.tsx | 2 +- components/charts/IPKChart.tsx | 2 +- components/charts/JenisPendaftaranChart.tsx | 2 +- .../JenisPendaftaranPerAngkatanChart.tsx | 2 +- components/charts/LulusTepatWaktuChart.tsx | 2 +- components/charts/LulusTepatWaktuPieChart.tsx | 2 +- components/charts/StatistikMahasiswaChart.tsx | 2 +- .../charts/StatistikPerAngkatanChart.tsx | 2 +- components/charts/StatusMahasiswaChart.tsx | 2 +- .../chartsDashboard/DistribusiIPKChart.tsx | 4 +- .../DistribusiIPKChartPerangkatan.tsx | 2 +- .../chartsDashboard/MKBelumDiambilChart.tsx | 491 +++++++++++++++++ .../chartsDashboard/NamaBeasiswaDashChart.tsx | 2 +- .../NamaBeasiswaDashPieChartPerangkatan.tsx | 2 +- .../ProvinsiMahasiswaPieChart.tsx | 2 +- .../StatusMahasiswaPieChartPerangkatan.tsx | 2 +- .../chartsDashboard/TerancamDOChart.tsx | 4 +- .../TingkatPrestasiDashChart.tsx | 2 +- .../TingkatPrestasiPieChartDash.tsx | 2 +- .../chartsDashboard/kkdashboardchart.tsx | 2 +- .../kkdashboardpiechartperangkatan.tsx | 2 +- .../kkdashboardtepatpiechart.tsx | 2 +- .../chartsDashboard/masastudiluluschart.tsx | 2 +- .../chartstable/tabelmkbelumdiambil.tsx | 494 ++++++++++++++++++ .../chartstable/tabelnamaterancamdo.tsx | 4 +- 33 files changed, 1437 insertions(+), 58 deletions(-) create mode 100644 app/api/mahasiswa/mkbelumdiambil/route.ts create mode 100644 app/api/tabeldetail/mk-belum-diambil/route.ts create mode 100644 app/detail/mk-belum-diambil/page.tsx create mode 100644 components/chartsDashboard/MKBelumDiambilChart.tsx create mode 100644 components/chartstable/tabelmkbelumdiambil.tsx diff --git a/app/api/mahasiswa/mkbelumdiambil/route.ts b/app/api/mahasiswa/mkbelumdiambil/route.ts new file mode 100644 index 0000000..e3fe66e --- /dev/null +++ b/app/api/mahasiswa/mkbelumdiambil/route.ts @@ -0,0 +1,156 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MKBelumDiambilData { + tahun_angkatan: number; + jenis_mk: string; + jumlah_belum_diambil: number; + total_mata_kuliah: number; + total_mahasiswa: number; +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + // 1. Ambil semua mata kuliah + const { data: mataKuliah, error: errMK } = await supabase + .from('mata_kuliah') + .select('id_mk, jenis_mk'); + + if (errMK) { + console.error('Error fetching mata kuliah:', errMK); + throw errMK; + } + + // 2. Hitung total mata kuliah per kategori (total_mk CTE) + const totalMKPerJenis = new Map(); + mataKuliah?.forEach((mk) => { + const jenis = mk.jenis_mk; + totalMKPerJenis.set(jenis, (totalMKPerJenis.get(jenis) || 0) + 1); + }); + + // 3. Ambil data mahasiswa + let mahasiswaQuery = supabase + .from('mahasiswa') + .select('id_mahasiswa, tahun_angkatan'); + + // 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) { + console.error('Error fetching mahasiswa:', errMhs); + throw errMhs; + } + + // 4. Hitung total mahasiswa per angkatan (total_mhs CTE) + const totalMhsPerAngkatan = new Map(); + mahasiswa?.forEach((m) => { + const tahun = m.tahun_angkatan; + if (tahun) { + totalMhsPerAngkatan.set(tahun, (totalMhsPerAngkatan.get(tahun) || 0) + 1); + } + }); + + // 5. Ambil semua nilai mahasiswa + const { data: nilaiMahasiswa, error: errNilai } = await supabase + .from('nilai_mahasiswa') + .select('id_mahasiswa, id_mk, nilai_huruf'); + + if (errNilai) { + console.error('Error fetching nilai mahasiswa:', errNilai); + throw errNilai; + } + + // 6. Buat Map untuk cek apakah mahasiswa sudah mengambil MK (dengan nilai != E) + // Key: "id_mahasiswa-id_mk", Value: true jika sudah diambil dan nilai != E + const sudahAmbilMap = new Map(); + nilaiMahasiswa?.forEach((nilai) => { + // Hanya dianggap sudah ambil jika nilai != E + if (nilai.nilai_huruf !== 'E') { + const key = `${nilai.id_mahasiswa}-${nilai.id_mk}`; + sudahAmbilMap.set(key, true); + } + }); + + // 7. Hitung mata kuliah belum diambil per angkatan per jenis MK (belum_ambil CTE) + // Map dengan key yang unik menggunakan object + const belumAmbilMap = new Map(); + + // CROSS JOIN mahasiswa dengan mata_kuliah + mahasiswa?.forEach((mhs) => { + if (!mhs.tahun_angkatan) return; + + mataKuliah?.forEach((mk) => { + const key = `${mhs.id_mahasiswa}-${mk.id_mk}`; + + // WHERE NOT EXISTS: jika tidak ada di sudahAmbilMap, berarti belum diambil + if (!sudahAmbilMap.has(key)) { + // Gunakan separator yang aman (||) untuk menghindari konflik dengan nama jenis_mk + const groupKey = `${mhs.tahun_angkatan}||${mk.jenis_mk}`; + const existing = belumAmbilMap.get(groupKey); + + if (existing) { + existing.jumlah += 1; + } else { + belumAmbilMap.set(groupKey, { + tahun: mhs.tahun_angkatan, + jenis: mk.jenis_mk, + jumlah: 1 + }); + } + } + }); + }); + + // 8. Gabungkan semua data (JOIN) + const results: MKBelumDiambilData[] = []; + + belumAmbilMap.forEach((value) => { + const tahun_angkatan = value.tahun; + const jenis_mk = value.jenis; + const jumlah_belum_diambil = value.jumlah; + + const total_mata_kuliah = totalMKPerJenis.get(jenis_mk) || 0; + const total_mahasiswa = totalMhsPerAngkatan.get(tahun_angkatan) || 0; + + results.push({ + tahun_angkatan, + jenis_mk, + jumlah_belum_diambil, + total_mata_kuliah, + total_mahasiswa, + }); + }); + + // 9. Urutkan berdasarkan tahun_angkatan dan jenis_mk + results.sort((a, b) => { + if (a.tahun_angkatan !== b.tahun_angkatan) { + return a.tahun_angkatan - b.tahun_angkatan; + } + // Urutkan jenis MK: Wajib, Pilihan Wajib, Pilihan + const jenisOrder = ['Wajib', 'Pilihan Wajib', 'Pilihan']; + return jenisOrder.indexOf(a.jenis_mk) - jenisOrder.indexOf(b.jenis_mk); + }); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in mkbelumdiambil API:', error); + return NextResponse.json( + { error: 'Internal Server Error', message: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} + diff --git a/app/api/mahasiswa/terancamdo/route.ts b/app/api/mahasiswa/terancamdo/route.ts index 704a408..e33cc32 100644 --- a/app/api/mahasiswa/terancamdo/route.ts +++ b/app/api/mahasiswa/terancamdo/route.ts @@ -122,11 +122,11 @@ export async function GET() { // 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) + // Evaluasi 4 semester: semester BETWEEN 3 AND 6 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 && + sem >= 3 && + sem <= 6 && sksTotal !== null && // Pastikan ada data nilai ( sksTotal < 40 || @@ -135,10 +135,10 @@ export async function GET() { ) { isDO = 1; } - // Evaluasi 8 semester: semester BETWEEN 8 AND 13 AND (sks_total < 80 OR ipk <= 2.50) + // Evaluasi 8 semester: semester BETWEEN 7 AND 11 AND (sks_total < 80 OR ipk <= 2.50) else if ( - sem >= 8 && - sem <= 13 && + sem >= 7 && + sem <= 11 && sksTotal !== null && // Pastikan ada data nilai ( sksTotal < 80 || @@ -147,10 +147,10 @@ export async function GET() { ) { 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) + // Evaluasi akhir masa studi: semester = 12 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 && + sem === 12 && sksTotal !== null && // Pastikan ada data nilai ( sksTotal < 144 || diff --git a/app/api/tabeldetail/mk-belum-diambil/route.ts b/app/api/tabeldetail/mk-belum-diambil/route.ts new file mode 100644 index 0000000..35daeee --- /dev/null +++ b/app/api/tabeldetail/mk-belum-diambil/route.ts @@ -0,0 +1,154 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MataKuliahBelumDiambil { + nama_mk: string; + jenis_mk: string; + kode_mk: string; + sks: number; + semester: number; +} + +interface MahasiswaMKBelumDiambil { + nim: string; + nama: string; + tahun_angkatan: number; + sks_total: number; + mk_belum_diambil: MataKuliahBelumDiambil[]; +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const jenisMK = searchParams.get('jenis_mk'); + + // 1. Ambil data mahasiswa + let mahasiswaQuery = supabase + .from('mahasiswa') + .select('id_mahasiswa, nim, nama, tahun_angkatan') + .order('tahun_angkatan', { ascending: true }) + .order('nim', { ascending: true }); + + // 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) { + console.error('Error fetching mahasiswa:', errMhs); + throw errMhs; + } + + // 2. Ambil semua mata kuliah + let mkQuery = supabase + .from('mata_kuliah') + .select('id_mk, kode_mk, nama_mk, jenis_mk, sks, semester') + .order('semester', { ascending: true }) + .order('nama_mk', { ascending: true }); + + // Filter jenis MK jika ada + if (jenisMK && jenisMK !== 'all') { + mkQuery = mkQuery.eq('jenis_mk', jenisMK); + } + + const { data: mataKuliah, error: errMK } = await mkQuery; + + if (errMK) { + console.error('Error fetching mata kuliah:', errMK); + throw errMK; + } + + // 3. Ambil semua nilai mahasiswa (hanya yang nilai != E, karena E dianggap belum lulus) + const { data: nilaiMahasiswa, error: errNilai } = await supabase + .from('nilai_mahasiswa') + .select('id_mahasiswa, id_mk, nilai_huruf'); + + if (errNilai) { + console.error('Error fetching nilai mahasiswa:', errNilai); + throw errNilai; + } + + // 4. Buat Map untuk cek MK yang sudah diambil (nilai != E) + // Key: "id_mahasiswa-id_mk", Value: true + const sudahAmbilMap = new Map(); + nilaiMahasiswa?.forEach((nilai) => { + // Hanya dianggap sudah ambil jika nilai != E + if (nilai.nilai_huruf !== 'E') { + const key = `${nilai.id_mahasiswa}-${nilai.id_mk}`; + sudahAmbilMap.set(key, true); + } + }); + + // 5. Hitung SKS total per mahasiswa (dari MK yang sudah lulus, nilai != E) + const sksTotalMap = new Map(); + nilaiMahasiswa?.forEach((nilai) => { + if (nilai.nilai_huruf !== 'E') { + const mk = mataKuliah?.find(m => m.id_mk === nilai.id_mk); + if (mk) { + const currentSKS = sksTotalMap.get(nilai.id_mahasiswa) || 0; + sksTotalMap.set(nilai.id_mahasiswa, currentSKS + mk.sks); + } + } + }); + + // 6. Proses data mahasiswa dan MK belum diambil + const results: MahasiswaMKBelumDiambil[] = []; + + mahasiswa?.forEach((mhs) => { + const mkBelumDiambil: MataKuliahBelumDiambil[] = []; + + // CROSS JOIN: cek setiap MK untuk mahasiswa ini + mataKuliah?.forEach((mk) => { + const key = `${mhs.id_mahasiswa}-${mk.id_mk}`; + + // WHERE nm.id_mk IS NULL: jika tidak ada di sudahAmbilMap, berarti belum diambil + if (!sudahAmbilMap.has(key)) { + mkBelumDiambil.push({ + nama_mk: mk.nama_mk, + jenis_mk: mk.jenis_mk, + kode_mk: mk.kode_mk, + sks: mk.sks, + semester: mk.semester + }); + } + }); + + // Hanya tambahkan jika ada MK yang belum diambil + if (mkBelumDiambil.length > 0) { + results.push({ + nim: mhs.nim, + nama: mhs.nama, + tahun_angkatan: mhs.tahun_angkatan, + sks_total: sksTotalMap.get(mhs.id_mahasiswa) || 0, + mk_belum_diambil: mkBelumDiambil + }); + } + }); + + // 7. Urutkan berdasarkan tahun_angkatan dan nim + results.sort((a, b) => { + if (a.tahun_angkatan !== b.tahun_angkatan) { + return a.tahun_angkatan - b.tahun_angkatan; + } + return a.nim.localeCompare(b.nim); + }); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in mk-belum-diambil API:', error); + return NextResponse.json( + { error: 'Internal Server Error', message: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} + diff --git a/app/api/tabeldetail/terancam-do/route.ts b/app/api/tabeldetail/terancam-do/route.ts index d0d8d32..b71242e 100644 --- a/app/api/tabeldetail/terancam-do/route.ts +++ b/app/api/tabeldetail/terancam-do/route.ts @@ -132,10 +132,10 @@ export async function GET(request: NextRequest) { const alasanList: string[] = []; let isDO = false; - // Evaluasi 4 semester: semester BETWEEN 4 AND 7 AND (sks_total < 40 OR ipk <= 2.50) + // Evaluasi 4 semester: semester BETWEEN 3 AND 6 AND (sks_total < 40 OR ipk <= 2.50) if ( - sem >= 4 && - sem <= 7 && + sem >= 3 && + sem <= 6 && sksTotal !== null && ( sksTotal < 40 || @@ -144,16 +144,16 @@ export async function GET(request: NextRequest) { ) { isDO = true; if (sksTotal < 40) { - alasanList.push('SKS kurang dari 40 pada evaluasi semester 4'); + alasanList.push('SKS kurang dari 40 pada evaluasi semester 3'); } if (ipk !== null && ipk <= 2.50) { - alasanList.push('IPK kurang dari 2.50 pada evaluasi semester 4'); + alasanList.push('IPK kurang dari 2.50 pada evaluasi semester 3'); } } - // Evaluasi 8 semester: semester BETWEEN 8 AND 13 AND (sks_total < 80 OR ipk <= 2.50) + // Evaluasi 8 semester: semester BETWEEN 7 AND 11 AND (sks_total < 80 OR ipk <= 2.50) else if ( - sem >= 8 && - sem <= 13 && + sem >= 7 && + sem <= 11 && sksTotal !== null && ( sksTotal < 80 || @@ -162,15 +162,15 @@ export async function GET(request: NextRequest) { ) { isDO = true; if (sksTotal < 80) { - alasanList.push('SKS kurang dari 80 pada evaluasi semester 8'); + alasanList.push('SKS kurang dari 80 pada evaluasi semester 7'); } if (ipk !== null && ipk <= 2.50) { - alasanList.push('IPK kurang dari 2.50 pada evaluasi semester 8'); + alasanList.push('IPK kurang dari 2.50 pada evaluasi semester 7'); } } // Evaluasi akhir masa studi: semester = 14 else if ( - sem === 14 && + sem === 12 && sksTotal !== null && ( sksTotal < 144 || @@ -184,25 +184,25 @@ export async function GET(request: NextRequest) { ) { isDO = true; if (sksTotal < 144) { - alasanList.push('Belum mencapai 144 SKS pada akhir masa studi'); + alasanList.push('Belum mencapai 144 SKS pada evaluasi semester 12'); } if (ipk !== null && ipk <= 2.00) { - alasanList.push('IPK di bawah 2.00 pada akhir masa studi'); + alasanList.push('IPK di bawah 2.00 pada evaluasi semester 12'); } if (jumlahE !== null && jumlahE > 0) { - alasanList.push('Memiliki nilai E pada akhir masa studi'); + alasanList.push('Memiliki nilai E pada evaluasi semester 12'); } if (sksD !== null && sksD > 14) { - alasanList.push('Total SKS dari nilai D lebih dari 14 SKS'); + alasanList.push('Total SKS dari nilai D lebih dari 14 SKS pada evaluasi semester 12'); } if (minWajib !== null && minWajib < 2.00) { - alasanList.push('Nilai minimal mata kuliah wajib di bawah C'); + alasanList.push('Nilai minimal mata kuliah wajib di bawah C pada evaluasi semester 12'); } if (lulusTA1 === 0) { - alasanList.push('Belum lulus Tugas Akhir 1 (INF-55201-406)'); + alasanList.push('Belum lulus Tugas Akhir 1 (INF-55201-406) pada evaluasi semester 12'); } if (lulusTA2 === 0) { - alasanList.push('Belum lulus Tugas Akhir 2 (INF-55201-407)'); + alasanList.push('Belum lulus Tugas Akhir 2 (INF-55201-407) pada evaluasi semester 12'); } } diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 9464670..49dfaaa 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -26,6 +26,7 @@ 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 MKBelumDiambilChart from "@/components/chartsDashboard/MKBelumDiambilChart"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { @@ -130,9 +131,11 @@ export default function TotalMahasiswaPage() { { id: 'academic', label: 'IPK & Jenis Pendaftaran' }, { id: 'study-duration', label: 'Kelulusan Tepat Waktu & Masa Studi' }, { id: 'expertise', label: 'Kelompok Keahlian' }, + { id: 'dropout', label: 'Terancam Drop Out & Distribusi IPK' }, { id: 'scholarship', label: 'Beasiswa & Prestasi' }, { id: 'demographics', label: 'Asal Kabupaten & Provinsi' }, - { id: 'bimbingan-dosen', label: 'Bimbingan Dosen' } + { id: 'bimbingan-dosen', label: 'Bimbingan Dosen' }, + { id: 'mk-belum-diambil', label: 'Mata Kuliah Belum Diambil' } ]; // Navigation menu items for per year data @@ -218,6 +221,10 @@ export default function TotalMahasiswaPage() {
+
+ + {/* Dropout Section */} +
@@ -234,6 +241,11 @@ export default function TotalMahasiswaPage() { + {/* MK Belum Diambil Section */} +
+ +
+ {/* Demographics Section */}
diff --git a/app/detail/mk-belum-diambil/page.tsx b/app/detail/mk-belum-diambil/page.tsx new file mode 100644 index 0000000..2d556e6 --- /dev/null +++ b/app/detail/mk-belum-diambil/page.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from "react"; +import MKBelumDiambilChart from "@/components/chartsDashboard/MKBelumDiambilChart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelMKBelumDiambil from "@/components/chartstable/tabelmkbelumdiambil"; + +export default function MKBelumDiambilDetailPage() { + const [selectedYear, setSelectedYear] = useState("all"); + const [selectedJenisMK, setSelectedJenisMK] = useState("all"); + + return ( +
+
+ {/* Filter Section */} + + + {/* Chart Section */} +
+ +
+ + {/* Tabel Section */} + + + {/* Information Section */} +
+

+ Informasi Visualisasi +

+
+
+

+ Grafik dan Tabel Mata Kuliah Belum Diambil +

+
    +
  • • Menampilkan data mahasiswa yang masih memiliki mata kuliah yang belum diambil atau belum lulus
  • +
  • • Grafik stacked bar horizontal menunjukkan jumlah MK belum diambil per jenis MK per tahun angkatan
  • +
  • • Mata kuliah dikategorikan menjadi tiga jenis:
  • +
  • - Wajib: Mata kuliah yang wajib diambil oleh semua mahasiswa
  • +
  • - Pilihan Wajib: Mata kuliah pilihan yang wajib diambil minimal satu dari kelompoknya
  • +
  • - Pilihan: Mata kuliah pilihan bebas
  • +
  • • Mata kuliah dianggap belum diambil jika:
  • +
  • - Mahasiswa belum pernah mengambil MK tersebut, ATAU
  • +
  • - Mahasiswa mendapat nilai E (gagal/tidak lulus)
  • +
  • • Tabel detail menampilkan daftar mahasiswa dengan:
  • +
  • - Informasi dasar mahasiswa (NIM, nama, angkatan)
  • +
  • - Total SKS yang sudah lulus (nilai != E)
  • +
  • - Jumlah MK yang belum diambil
  • +
  • - Dropdown detail MK belum diambil (kode MK, nama MK, jenis MK, SKS, semester)
  • +
  • • Filter tersedia untuk tahun angkatan dan jenis MK untuk analisis lebih spesifik
  • +
  • • Data dapat digunakan untuk monitoring progres akademik dan identifikasi mahasiswa yang perlu bimbingan
  • +
+
+
+
+
+
+ ); +} + diff --git a/app/detail/terancam-do/page.tsx b/app/detail/terancam-do/page.tsx index 0aa565d..943bd5b 100644 --- a/app/detail/terancam-do/page.tsx +++ b/app/detail/terancam-do/page.tsx @@ -42,10 +42,10 @@ export default function TerancamDODetailPage() {
  • • 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
  • +
  • • Kriteria evaluasi terdiri dari tiga tahap (evaluasi dilakukan sebelum semester target):
  • +
  • - Evaluasi semester 3: SKS minimal 40 dan IPK > 2.50 (sebelum masuk semester 4)
  • +
  • - Evaluasi semester 7: SKS minimal 80 dan IPK > 2.50 (sebelum masuk semester 8)
  • +
  • - Evaluasi semester 12: SKS minimal 144, IPK > 2.00, tidak ada nilai E, nilai D maksimal 10%, nilai mata kuliah wajib minimal C, dan lulus tugas akhir (sebelum semester 14)
  • • 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/charts/BimbinganDosenChart.tsx b/components/charts/BimbinganDosenChart.tsx index b4e4d41..41c0c83 100644 --- a/components/charts/BimbinganDosenChart.tsx +++ b/components/charts/BimbinganDosenChart.tsx @@ -375,7 +375,7 @@ export default function BimbinganDosenChart({
- Bimbingan Dosen + Jumlah Bimbingan Dosen Berdasarkan Nama Dosen dan Status Bimbingan {showDetailButton && ( diff --git a/components/charts/BimbinganDosenPerAngkatanChart.tsx b/components/charts/BimbinganDosenPerAngkatanChart.tsx index 890677a..8caba91 100644 --- a/components/charts/BimbinganDosenPerAngkatanChart.tsx +++ b/components/charts/BimbinganDosenPerAngkatanChart.tsx @@ -408,7 +408,7 @@ export default function BimbinganDosenPerAngkatanChart({ tahunAngkatan }: Props) - Bimbingan Dosen Angkatan {tahunAngkatan} + Jumlah Bimbingan Dosen Berdasarkan Nama Dosen dan Status Bimbingan Angkatan {tahunAngkatan} diff --git a/components/charts/IPKChart.tsx b/components/charts/IPKChart.tsx index 95a5a27..d00729e 100644 --- a/components/charts/IPKChart.tsx +++ b/components/charts/IPKChart.tsx @@ -332,7 +332,7 @@ export default function IPKChart({
- Rata-rata IPK Mahasiswa + Rata-rata IPK Mahasiswa Berdasarkan Tahun Angkatan {showDetailButton && ( diff --git a/components/charts/JenisPendaftaranChart.tsx b/components/charts/JenisPendaftaranChart.tsx index e56ea25..4652ee8 100644 --- a/components/charts/JenisPendaftaranChart.tsx +++ b/components/charts/JenisPendaftaranChart.tsx @@ -437,7 +437,7 @@ export default function JenisPendaftaranChart({
- Jenis Pendaftaran Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Jenis Pendaftaran dan Tahun Angkatan {showDetailButton && ( diff --git a/components/charts/JenisPendaftaranPerAngkatanChart.tsx b/components/charts/JenisPendaftaranPerAngkatanChart.tsx index 091293d..0b126b6 100644 --- a/components/charts/JenisPendaftaranPerAngkatanChart.tsx +++ b/components/charts/JenisPendaftaranPerAngkatanChart.tsx @@ -227,7 +227,7 @@ export default function JenisPendaftaranPerAngkatanChart({ tahunAngkatan }: Prop - Jenis Pendaftaran Angkatan {tahunAngkatan} + Persentase Jumlah Mahasiswa Berdasarkan Jenis Pendaftaran Angkatan {tahunAngkatan} diff --git a/components/charts/LulusTepatWaktuChart.tsx b/components/charts/LulusTepatWaktuChart.tsx index d16ce1c..cd7cb9d 100644 --- a/components/charts/LulusTepatWaktuChart.tsx +++ b/components/charts/LulusTepatWaktuChart.tsx @@ -254,7 +254,7 @@ export default function LulusTepatWaktuChart({
- Mahasiswa Lulus Tepat Waktu + Jumlah Mahasiswa Lulus Tepat Waktu Berdasarkan Tahun Angkatan {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} {showDetailButton && ( diff --git a/components/charts/LulusTepatWaktuPieChart.tsx b/components/charts/LulusTepatWaktuPieChart.tsx index cd6dc94..cc66ceb 100644 --- a/components/charts/LulusTepatWaktuPieChart.tsx +++ b/components/charts/LulusTepatWaktuPieChart.tsx @@ -158,7 +158,7 @@ export default function LulusTepatWaktuPieChart({ selectedYear }: Props) { - Mahasiswa Lulus Tepat Waktu + Jumlah Mahasiswa Lulus Tepat Waktu {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''}
diff --git a/components/charts/StatistikMahasiswaChart.tsx b/components/charts/StatistikMahasiswaChart.tsx index 5334655..f2920cd 100644 --- a/components/charts/StatistikMahasiswaChart.tsx +++ b/components/charts/StatistikMahasiswaChart.tsx @@ -441,7 +441,7 @@ export default function StatistikMahasiswaChart({
- Jumlah Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Gender dan Tahun Angkatan {showDetailButton && ( diff --git a/components/charts/StatistikPerAngkatanChart.tsx b/components/charts/StatistikPerAngkatanChart.tsx index 0ba2efb..db32fc8 100644 --- a/components/charts/StatistikPerAngkatanChart.tsx +++ b/components/charts/StatistikPerAngkatanChart.tsx @@ -216,7 +216,7 @@ export default function StatistikPerAngkatanChart({ tahunAngkatan }: Props) { - Jumlah Mahasiswa Angkatan {tahunAngkatan} + Persentase Jumlah Mahasiswa Berdasarkan Gender dan Tahun Angkatan {tahunAngkatan} diff --git a/components/charts/StatusMahasiswaChart.tsx b/components/charts/StatusMahasiswaChart.tsx index bdda18c..9c3bfe4 100644 --- a/components/charts/StatusMahasiswaChart.tsx +++ b/components/charts/StatusMahasiswaChart.tsx @@ -290,7 +290,7 @@ export default function StatusMahasiswaChart({
- Status Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Status Kuliah dan Tahun Angkatan {showDetailButton && ( diff --git a/components/chartsDashboard/DistribusiIPKChart.tsx b/components/chartsDashboard/DistribusiIPKChart.tsx index 4514735..7551b99 100644 --- a/components/chartsDashboard/DistribusiIPKChart.tsx +++ b/components/chartsDashboard/DistribusiIPKChart.tsx @@ -285,7 +285,7 @@ export default function DistribusiIPKChart({ - Memuat data distribusi IPK... + Loading... @@ -327,7 +327,7 @@ export default function DistribusiIPKChart({
- Distribusi IPK + Persentase Distribusi IPK Berdasarkan Tahun Angkatan {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} {showDetailButton && ( diff --git a/components/chartsDashboard/DistribusiIPKChartPerangkatan.tsx b/components/chartsDashboard/DistribusiIPKChartPerangkatan.tsx index f6f5eca..f87d6b3 100644 --- a/components/chartsDashboard/DistribusiIPKChartPerangkatan.tsx +++ b/components/chartsDashboard/DistribusiIPKChartPerangkatan.tsx @@ -199,7 +199,7 @@ export default function DistribusiIPKChartPerangkatan({ selectedYear }: Props) { - Distribusi IPK + Persentase Distribusi IPK {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} diff --git a/components/chartsDashboard/MKBelumDiambilChart.tsx b/components/chartsDashboard/MKBelumDiambilChart.tsx new file mode 100644 index 0000000..5b48216 --- /dev/null +++ b/components/chartsDashboard/MKBelumDiambilChart.tsx @@ -0,0 +1,491 @@ +'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"; +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 MKBelumDiambilData { + tahun_angkatan: number; + jenis_mk: string; + jumlah_belum_diambil: number; + total_mata_kuliah: number; + total_mahasiswa: number; +} + +interface MKBelumDiambilChartProps { + selectedYear?: string; + height?: string; + showDetailButton?: boolean; +} + +export default function MKBelumDiambilChart({ + selectedYear = 'all', + height = "h-[300px] sm:h-[350px] md:h-[300px]", + showDetailButton = true +}: MKBelumDiambilChartProps) { + const { theme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([]); + const [options, setOptions] = useState({ + chart: { + type: 'bar', + stacked: true, + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + barHeight: '70%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number, opts: any) { + 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 ''; + + 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: [], + title: { + text: 'Jumlah Mata Kuliah Belum Diambil', + 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: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + colors: ['#3B82F6', '#10B981', '#F59E0B'], // Wajib (blue), Pilihan Wajib (green), Pilihan (amber) + 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 jenisMKNames = w.config.series.map((s: any) => s.name); + const jenisMKColors = w.config.colors; + + let tooltipContent = ` +
+
Angkatan ${tahun}
`; + + // Tambahkan setiap jenis MK + 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 += ` +
+
+ ${jenisMKNames[index]} + ${value} (${percentage}%) +
`; + } + }); + + // Tambahkan total + tooltipContent += ` +
+
+ Total MK Belum Diambil + ${total} +
+
+ `; + + return tooltipContent; + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + formatter: function (val: number, opts: any) { + 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 ''; + + const percentage = ((val / totalForYear) * 100).toFixed(0); + return percentage + '%'; + }, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + legend: { + ...prev.legend, + labels: { + ...prev.legend?.labels, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light', + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const tahun = w.globals.labels[dataPointIndex]; + const isDark = currentTheme === 'dark'; + + // Hitung total untuk tahun ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const jenisMKNames = w.config.series.map((s: any) => s.name); + const jenisMKColors = w.config.colors; + + let tooltipContent = ` +
+
Angkatan ${tahun}
`; + + // Tambahkan setiap jenis MK + 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 += ` +
+
+ ${jenisMKNames[index]} + ${value} (${percentage}%) +
`; + } + }); + + // Tambahkan total + tooltipContent += ` +
+
+ Total MK Belum Diambil + ${total} +
+
+ `; + + return tooltipContent; + } + } + })); + }, [theme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = selectedYear === 'all' + ? '/api/mahasiswa/mkbelumdiambil' + : `/api/mahasiswa/mkbelumdiambil?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); + + // Ambil tahun angkatan unik dan urutkan + const tahunAngkatan = [...new Set(result.map(item => item.tahun_angkatan))].sort((a, b) => a - b); + + // Ambil jenis MK unik dan urutkan sesuai urutan: Wajib, Pilihan Wajib, Pilihan + const jenisMKOrder = ['Wajib', 'Pilihan Wajib', 'Pilihan']; + const jenisMK = [...new Set(result.map(item => item.jenis_mk))].sort((a, b) => { + return jenisMKOrder.indexOf(a) - jenisMKOrder.indexOf(b); + }); + + // Buat series data + const seriesData = jenisMK.map(jenis => ({ + name: jenis, + data: tahunAngkatan.map(tahun => { + const item = result.find(d => d.tahun_angkatan === tahun && d.jenis_mk === jenis); + return item ? item.jumlah_belum_diambil : 0; + }), + })); + + setSeries(seriesData); + + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: tahunAngkatan, + }, + })); + } 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 (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Memuat data mata kuliah belum diambil... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Mata Kuliah Belum Diambil + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +

+ Tidak ada data yang tersedia untuk periode ini. +

+
+
+ ); + } + + return ( + + +
+ + Mata Kuliah Belum Diambil Berdasarkan Jenis MK + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + {showDetailButton && ( + + + + )} +
+
+ +
+ +
+
+
+ ); +} + diff --git a/components/chartsDashboard/NamaBeasiswaDashChart.tsx b/components/chartsDashboard/NamaBeasiswaDashChart.tsx index cb9d445..e59afbb 100644 --- a/components/chartsDashboard/NamaBeasiswaDashChart.tsx +++ b/components/chartsDashboard/NamaBeasiswaDashChart.tsx @@ -329,7 +329,7 @@ export default function NamaBeasiswaDashChart({
- Nama Beasiswa Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Nama Beasiswa dan Tahun Angkatan {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} {showDetailButton && ( diff --git a/components/chartsDashboard/NamaBeasiswaDashPieChartPerangkatan.tsx b/components/chartsDashboard/NamaBeasiswaDashPieChartPerangkatan.tsx index a3f2f7b..7ceb69c 100644 --- a/components/chartsDashboard/NamaBeasiswaDashPieChartPerangkatan.tsx +++ b/components/chartsDashboard/NamaBeasiswaDashPieChartPerangkatan.tsx @@ -199,7 +199,7 @@ export default function NamaBeasiswaDashPieChartPerangkatan({ selectedYear }: Pr - Nama Beasiswa + Persentase Jumlah Mahasiswa Berdasarkan Nama Beasiswa {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} diff --git a/components/chartsDashboard/ProvinsiMahasiswaPieChart.tsx b/components/chartsDashboard/ProvinsiMahasiswaPieChart.tsx index f8ca1c4..73134db 100644 --- a/components/chartsDashboard/ProvinsiMahasiswaPieChart.tsx +++ b/components/chartsDashboard/ProvinsiMahasiswaPieChart.tsx @@ -171,7 +171,7 @@ export default function ProvinsiMahasiswaPieChart({
- Asal Provinsi Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Asal Provinsi {showDetailButton && ( diff --git a/components/chartsDashboard/StatusMahasiswaPieChartPerangkatan.tsx b/components/chartsDashboard/StatusMahasiswaPieChartPerangkatan.tsx index 4b36c4a..d4b084e 100644 --- a/components/chartsDashboard/StatusMahasiswaPieChartPerangkatan.tsx +++ b/components/chartsDashboard/StatusMahasiswaPieChartPerangkatan.tsx @@ -175,7 +175,7 @@ export default function StatusMahasiswaPieChartPerangkatan({ selectedYear }: Pro - Status Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Status Kuliah {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} diff --git a/components/chartsDashboard/TerancamDOChart.tsx b/components/chartsDashboard/TerancamDOChart.tsx index 22ec065..05ca4f2 100644 --- a/components/chartsDashboard/TerancamDOChart.tsx +++ b/components/chartsDashboard/TerancamDOChart.tsx @@ -177,7 +177,7 @@ export default function TerancamDOChart({ - Memuat data terancam DO... + Loading... @@ -223,7 +223,7 @@ export default function TerancamDOChart({
- Mahasiswa Terancam Drop Out + Jumlah Mahasiswa Terancam Drop Out Berdasarkan Tahun Angkatan {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} {showDetailButton && ( diff --git a/components/chartsDashboard/TingkatPrestasiDashChart.tsx b/components/chartsDashboard/TingkatPrestasiDashChart.tsx index c4e08af..2e60bdf 100644 --- a/components/chartsDashboard/TingkatPrestasiDashChart.tsx +++ b/components/chartsDashboard/TingkatPrestasiDashChart.tsx @@ -314,7 +314,7 @@ export default function TingkatPrestasiDashChart({
- Tingkat Prestasi Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Tingkat Prestasi dan Tahun Angkatan {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} {showDetailButton && ( diff --git a/components/chartsDashboard/TingkatPrestasiPieChartDash.tsx b/components/chartsDashboard/TingkatPrestasiPieChartDash.tsx index c6d0037..6b1df6c 100644 --- a/components/chartsDashboard/TingkatPrestasiPieChartDash.tsx +++ b/components/chartsDashboard/TingkatPrestasiPieChartDash.tsx @@ -166,7 +166,7 @@ export default function TingkatPrestasiPieChart({ selectedYear }: Props) { - Tingkat Prestasi + Persentase Jumlah Mahasiswa Berdasarkan Tingkat Prestasi {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} diff --git a/components/chartsDashboard/kkdashboardchart.tsx b/components/chartsDashboard/kkdashboardchart.tsx index 4ef959d..9a6de4e 100644 --- a/components/chartsDashboard/kkdashboardchart.tsx +++ b/components/chartsDashboard/kkdashboardchart.tsx @@ -285,7 +285,7 @@ export default function KelompokKeahlianStatusChart({
- Kelompok Keahlian Mahasiswa + Persentase Jumlah Mahasiswa Berdasarkan Kelompok Keahlian dan Tahun Angkatan {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} {showDetailButton && ( diff --git a/components/chartsDashboard/kkdashboardpiechartperangkatan.tsx b/components/chartsDashboard/kkdashboardpiechartperangkatan.tsx index 5a547e8..fa60456 100644 --- a/components/chartsDashboard/kkdashboardpiechartperangkatan.tsx +++ b/components/chartsDashboard/kkdashboardpiechartperangkatan.tsx @@ -166,7 +166,7 @@ export default function KelompokKeahlianPieChartPerAngkatan({ selectedYear }: Pr - Kelompok Keahlian + Persentase Jumlah Mahasiswa Berdasarkan Kelompok Keahlian {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} diff --git a/components/chartsDashboard/kkdashboardtepatpiechart.tsx b/components/chartsDashboard/kkdashboardtepatpiechart.tsx index b158b5e..c0b9442 100644 --- a/components/chartsDashboard/kkdashboardtepatpiechart.tsx +++ b/components/chartsDashboard/kkdashboardtepatpiechart.tsx @@ -170,7 +170,7 @@ export default function KelompokKeahlianLulusTepatPieChart({
- Kelompok Keahlian Lulusan Tepat Waktu + Persentase Jumlah Mahasiswa Lulus Tepat Waktu Berdasarkan Kelompok Keahlian {showDetailButton && ( diff --git a/components/chartsDashboard/masastudiluluschart.tsx b/components/chartsDashboard/masastudiluluschart.tsx index 808992a..dba58c8 100644 --- a/components/chartsDashboard/masastudiluluschart.tsx +++ b/components/chartsDashboard/masastudiluluschart.tsx @@ -261,7 +261,7 @@ export default function MasaStudiLulusChart({
- Rata-rata Masa Studi Mahasiswa Lulus + Rata-rata Masa Studi Mahasiswa Lulus Berdasarkan Tahun Angkatan {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} {showDetailButton && ( diff --git a/components/chartstable/tabelmkbelumdiambil.tsx b/components/chartstable/tabelmkbelumdiambil.tsx new file mode 100644 index 0000000..6275314 --- /dev/null +++ b/components/chartstable/tabelmkbelumdiambil.tsx @@ -0,0 +1,494 @@ +'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 { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Loader2, BookOpen, ChevronDown } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; + +interface MataKuliahBelumDiambil { + nama_mk: string; + jenis_mk: string; + kode_mk: string; + sks: number; + semester: number; +} + +interface MahasiswaMKBelumDiambil { + nim: string; + nama: string; + tahun_angkatan: number; + sks_total: number; + mk_belum_diambil: MataKuliahBelumDiambil[]; +} + +interface TabelMKBelumDiambilProps { + selectedYear: string; + selectedJenisMK: string; + onJenisMKChange: (jenis: string) => void; +} + +export default function TabelMKBelumDiambil({ + selectedYear, + selectedJenisMK, + onJenisMKChange +}: TabelMKBelumDiambilProps) { + 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/mk-belum-diambil'; + const params = new URLSearchParams(); + + if (selectedYear && selectedYear !== 'all') { + params.append('tahun_angkatan', selectedYear); + } + if (selectedJenisMK && selectedJenisMK !== 'all') { + params.append('jenis_mk', selectedJenisMK); + } + + if (params.toString()) { + url += `?${params.toString()}`; + } + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa MK belum diambil data'); + } + + const data = await response.json(); + setMahasiswaData(data); + setCurrentPage(1); // Reset ke halaman pertama saat filter berubah + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisMK]); + + // 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); + }; + + // Get badge color based on jenis_mk + const getJenisMKBadgeColor = (jenis: string) => { + switch (jenis) { + case 'Wajib': + return 'bg-blue-500 hover:bg-blue-600'; + case 'Pilihan Wajib': + return 'bg-green-500 hover:bg-green-600'; + case 'Pilihan': + return 'bg-amber-500 hover:bg-amber-600'; + default: + return 'bg-gray-500 hover:bg-gray-600'; + } + }; + + // Calculate statistics + const stats = { + total: mahasiswaData.length, + total_mk_belum_diambil: mahasiswaData.reduce((sum, mhs) => sum + mhs.mk_belum_diambil.length, 0), + avg_mk_belum_diambil: mahasiswaData.length > 0 + ? (mahasiswaData.reduce((sum, mhs) => sum + mhs.mk_belum_diambil.length, 0) / mahasiswaData.length).toFixed(1) + : 0, + }; + + // Get jenis MK counts + const jenisMKCounts = mahasiswaData.reduce((acc, mhs) => { + mhs.mk_belum_diambil.forEach(mk => { + acc[mk.jenis_mk] = (acc[mk.jenis_mk] || 0) + 1; + }); + return acc; + }, {} as Record); + + if (loading) { + return ( + + + +
+ + Memuat data... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Mahasiswa - Mata Kuliah Belum Diambil {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
+
+
Total Mahasiswa
+
{stats.total}
+
+
+
Total MK Belum Diambil
+
{stats.total_mk_belum_diambil}
+
+
+ {/* Tampilkan badge jenis MK */} + {selectedJenisMK === 'all' && Object.keys(jenisMKCounts).length > 0 && ( +
+ {Object.entries(jenisMKCounts) + .sort(([a], [b]) => { + const order = ['Wajib', 'Pilihan Wajib', 'Pilihan']; + return order.indexOf(a) - order.indexOf(b); + }) + .map(([jenis, count]) => ( + + {jenis}: {count} MK + + ))} +
+ )} +
+ + {/* Show entries selector and filter */} +
+
+ Show + + entries +
+ + {/* Filter Jenis MK */} +
+ Filter Jenis MK: + +
+
+ +
+ + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + SKS Total + Jumlah MK Belum Diambil + MK Belum Diambil + + + + {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.sks_total} + + + + {mahasiswa.mk_belum_diambil.length} MK + + + + + + + + + {mahasiswa.mk_belum_diambil.length === 0 ? ( + + Tidak ada MK yang belum diambil + + ) : ( + mahasiswa.mk_belum_diambil.map((mk, mkIndex) => ( + +
+ {mk.nama_mk} + + {mk.jenis_mk} + +
+
+ {mk.kode_mk} + + {mk.sks} SKS + + Semester {mk.semester} +
+
+ )) + )} +
+
+
+
+ )) + )} +
+
+
+ + {/* 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; + 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 index 04d39f9..1f14caa 100644 --- a/components/chartstable/tabelnamaterancamdo.tsx +++ b/components/chartstable/tabelnamaterancamdo.tsx @@ -188,7 +188,7 @@ export default function TabelNamaTerancamDO({ selectedYear }: TabelNamaTerancamD {/* Show entries selector */} -
+
Show