diff --git a/app/api/tabeldetail/ipk/route.ts b/app/api/tabeldetail/ipk/route.ts new file mode 100644 index 0000000..6e6cc82 --- /dev/null +++ b/app/api/tabeldetail/ipk/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +// GET - Ambil data mahasiswa dengan IPK dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select('nim, nama, 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 } + ); + } + + return NextResponse.json(data || [], { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/ipk API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/jenis-pendaftaran/route.ts b/app/api/tabeldetail/jenis-pendaftaran/route.ts new file mode 100644 index 0000000..b18564d --- /dev/null +++ b/app/api/tabeldetail/jenis-pendaftaran/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +// GET - Ambil data mahasiswa dengan jenis pendaftaran dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select('nim, nama, jenis_pendaftaran, tahun_angkatan') + .not('jenis_pendaftaran', 'is', null) // Hanya ambil mahasiswa yang memiliki jenis pendaftaran + .order('nama'); // Urutkan berdasarkan nama + + // 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 } + ); + } + + return NextResponse.json(data || [], { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/jenis-pendaftaran API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/kelompok-keahlian/route.ts b/app/api/tabeldetail/kelompok-keahlian/route.ts new file mode 100644 index 0000000..fe649d4 --- /dev/null +++ b/app/api/tabeldetail/kelompok-keahlian/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaKelompokKeahlian { + nim: string; + nama: string; + tahun_angkatan: number; + nama_kelompok_keahlian: string; +} + +// GET - Ambil data mahasiswa dengan kelompok keahlian dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select(` + nim, + nama, + tahun_angkatan, + kelompok_keahlian!id_kelompok_keahlian(nama_kelompok) + `) + .not('id_kelompok_keahlian', 'is', null) // Hanya ambil mahasiswa yang sudah memiliki kelompok keahlian + .order('nama', { ascending: true }); // Urutkan berdasarkan nama mahasiswa + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { message: 'Failed to fetch data', error: error.message }, + { status: 500 } + ); + } + + // Transform data untuk meratakan field yang di-join + const transformedData: MahasiswaKelompokKeahlian[] = (data || []).map((item: any) => ({ + nim: item.nim, + nama: item.nama, + tahun_angkatan: item.tahun_angkatan, + nama_kelompok_keahlian: item.kelompok_keahlian?.nama_kelompok || '' + })); + + 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/kelompok-keahlian API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/kk-lulus-tepat/route.ts b/app/api/tabeldetail/kk-lulus-tepat/route.ts new file mode 100644 index 0000000..00cfb49 --- /dev/null +++ b/app/api/tabeldetail/kk-lulus-tepat/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaKKLulusTepat { + nim: string; + nama: string; + tahun_angkatan: number; + nama_kelompok_keahlian: string; + semester: number; +} + +// GET - Ambil data mahasiswa lulus tepat waktu dengan kelompok keahlian dan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select(` + nim, + nama, + tahun_angkatan, + semester, + kelompok_keahlian!inner(nama_kelompok) + `) + .eq('status_kuliah', 'Lulus') // Hanya mahasiswa yang sudah lulus + .lte('semester', 8) // Lulus tepat waktu (maksimal 8 semester) + .order('semester', { ascending: true }); // Urutkan berdasarkan semester tercepat + + // Jika ada filter tahun angkatan, terapkan filter + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { message: 'Failed to fetch data', error: error.message }, + { status: 500 } + ); + } + + // Transform data untuk meratakan field yang di-join + const transformedData: MahasiswaKKLulusTepat[] = (data || []).map((item: any) => ({ + nim: item.nim, + nama: item.nama, + tahun_angkatan: item.tahun_angkatan, + semester: item.semester, + nama_kelompok_keahlian: item.kelompok_keahlian?.nama_kelompok || '' + })); + + 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/kk-lulus-tepat API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/lulus-tepat-waktu/route.ts b/app/api/tabeldetail/lulus-tepat-waktu/route.ts new file mode 100644 index 0000000..5587cfc --- /dev/null +++ b/app/api/tabeldetail/lulus-tepat-waktu/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +// GET - Ambil data mahasiswa lulus tepat waktu dengan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select('nim, nama, tahun_angkatan, ipk, semester') + .eq('status_kuliah', 'Lulus') + .lte('semester', 8) // Lulus tepat waktu maksimal 8 semester (4 tahun) + .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 } + ); + } + + return NextResponse.json(data || [], { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail/lulus-tepat-waktu API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/masa-studi/route.ts b/app/api/tabeldetail/masa-studi/route.ts new file mode 100644 index 0000000..b37937d --- /dev/null +++ b/app/api/tabeldetail/masa-studi/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MahasiswaMasaStudi { + nim: string; + nama: string; + tahun_angkatan: number; + semester: number; + status_kuliah: string; + masa_studi_tahun: number; +} + +// GET - Ambil data mahasiswa masa studi dengan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select('nim, nama, tahun_angkatan, semester, status_kuliah') + .eq('status_kuliah', 'Lulus') // Hanya ambil mahasiswa yang sudah lulus + .not('semester', 'is', null) // Hanya ambil mahasiswa yang memiliki data semester + .order('semester', { ascending: false }); // Urutkan berdasarkan semester 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 } + ); + } + + // Transform data untuk menghitung masa studi dalam tahun + const transformedData: MahasiswaMasaStudi[] = (data || []).map((item: any) => ({ + nim: item.nim, + nama: item.nama, + tahun_angkatan: item.tahun_angkatan, + semester: item.semester, + status_kuliah: item.status_kuliah, + // Hitung masa studi dalam tahun berdasarkan semester (semester / 2) + masa_studi_tahun: Math.round(((item.semester || 0) / 2) * 10) / 10 + })); + + // Urutkan berdasarkan masa studi terlama + transformedData.sort((a, b) => b.masa_studi_tahun - a.masa_studi_tahun); + + 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/masa-studi API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/tabeldetail/route.ts b/app/api/tabeldetail/route.ts new file mode 100644 index 0000000..c95c047 --- /dev/null +++ b/app/api/tabeldetail/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +// GET - Ambil data mahasiswa dengan filter tahun angkatan untuk tabel detail +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select('nim, nama, status_kuliah, tahun_angkatan') + .order('nama'); + + // 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 } + ); + } + + return NextResponse.json(data || [], { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + } catch (error) { + console.error('Error in tabeldetail API:', error); + return NextResponse.json( + { message: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/detail/jenis-pendaftaran/page.tsx b/app/detail/jenis-pendaftaran/page.tsx index f7328e9..256917c 100644 --- a/app/detail/jenis-pendaftaran/page.tsx +++ b/app/detail/jenis-pendaftaran/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart"; import JenisPendaftaranPerAngkatanChart from "@/components/charts/JenisPendaftaranPerAngkatanChart"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelJenisPendaftaranMahasiswa from "@/components/chartstable/tabeljenisPendaftaranmahasiswa"; export default function JenisPendaftaranDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -36,6 +37,9 @@ export default function JenisPendaftaranDetailPage() { )} + {/* Tabel Section */} + + {/* Information Section */}

diff --git a/app/detail/kelompok-keahlian-lulus-tepat/page.tsx b/app/detail/kelompok-keahlian-lulus-tepat/page.tsx index 1f3fc96..c8f48fb 100644 --- a/app/detail/kelompok-keahlian-lulus-tepat/page.tsx +++ b/app/detail/kelompok-keahlian-lulus-tepat/page.tsx @@ -1,11 +1,22 @@ 'use client'; +import { useState } from "react"; import KelompokKeahlianLulusTepatPieChart from "@/components/chartsDashboard/kkdashboardtepatpiechart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelKKLulusTepatMahasiswa from "@/components/chartstable/tabelkklulustepatmahasiswa"; export default function KelompokKeahlianLulusTepatDetailPage() { + const [selectedYear, setSelectedYear] = useState("all"); + return (
+ {/* Filter Section */} + + {/* Chart Section - Enhanced Size */}
{/* Kelompok Keahlian Lulus Tepat Chart dengan ukuran lebih besar */} @@ -17,6 +28,9 @@ export default function KelompokKeahlianLulusTepatDetailPage() {
+ {/* Tabel Section */} + + {/* Information Section */}

diff --git a/app/detail/kelompok-keahlian/page.tsx b/app/detail/kelompok-keahlian/page.tsx index f4e962a..4975947 100644 --- a/app/detail/kelompok-keahlian/page.tsx +++ b/app/detail/kelompok-keahlian/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import KelompokKeahlianStatusChart from "@/components/chartsDashboard/kkdashboardchart"; import KelompokKeahlianPieChartPerAngkatan from "@/components/chartsDashboard/kkdashboardpiechartperangkatan"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelKelompokKeahlianMahasiswa from "@/components/chartstable/tabelkelompokkeahliamahasiswa"; export default function KelompokKeahlianDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -37,6 +38,9 @@ export default function KelompokKeahlianDetailPage() { )}

+ {/* Tabel Section */} + + {/* Information Section */}

diff --git a/app/detail/lulus-tepat-waktu/page.tsx b/app/detail/lulus-tepat-waktu/page.tsx index 123b252..565e0b9 100644 --- a/app/detail/lulus-tepat-waktu/page.tsx +++ b/app/detail/lulus-tepat-waktu/page.tsx @@ -1,23 +1,37 @@ 'use client'; +import { useState } from "react"; import LulusTepatWaktuChart from "@/components/charts/LulusTepatWaktuChart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelLulusTepatWaktuMahasiswa from "@/components/chartstable/tabellulustepatwaktumahasiswa"; export default function LulusTepatWaktuDetailPage() { + const [selectedYear, setSelectedYear] = useState("all"); + return (
+ {/* Filter Section */} + + {/* Chart Section - Enhanced Size */}
{/* Lulus Tepat Waktu Chart dengan ukuran lebih besar */}
+ {/* Tabel Section */} + + {/* Information Section */}

diff --git a/app/detail/masa-studi-lulus/page.tsx b/app/detail/masa-studi-lulus/page.tsx index 901621a..ef32e22 100644 --- a/app/detail/masa-studi-lulus/page.tsx +++ b/app/detail/masa-studi-lulus/page.tsx @@ -1,23 +1,37 @@ 'use client'; +import { useState } from "react"; import MasaStudiLulusChart from "@/components/chartsDashboard/masastudiluluschart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelMasaStudiMahasiswa from "@/components/chartstable/tabelmasastudimahasiswa"; export default function MasaStudiLulusDetailPage() { + const [selectedYear, setSelectedYear] = useState("all"); + return (
+ {/* Filter Section */} + + {/* Chart Section - Enhanced Size */}
{/* Masa Studi Lulus Chart dengan ukuran lebih besar */}
+ {/* Tabel Section */} + + {/* Information Section */}

diff --git a/app/detail/rata-rata-ipk/page.tsx b/app/detail/rata-rata-ipk/page.tsx index a85f683..4a95a0a 100644 --- a/app/detail/rata-rata-ipk/page.tsx +++ b/app/detail/rata-rata-ipk/page.tsx @@ -1,11 +1,22 @@ 'use client'; +import { useState } from "react"; import IPKChart from "@/components/charts/IPKChart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelIPKMahasiswa from "@/components/chartstable/tabelipkmahasiswa"; export default function RataRataIPKDetailPage() { + const [selectedYear, setSelectedYear] = useState("all"); + return (
+ {/* Filter Section */} + + {/* Chart Section - Enhanced Size */}
{/* IPK Chart dengan ukuran lebih besar */} @@ -17,6 +28,9 @@ export default function RataRataIPKDetailPage() {
+ {/* Tabel Section */} + + {/* Information Section */}

diff --git a/app/detail/status-mahasiswa/page.tsx b/app/detail/status-mahasiswa/page.tsx index eb496fe..1550fd7 100644 --- a/app/detail/status-mahasiswa/page.tsx +++ b/app/detail/status-mahasiswa/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import StatusMahasiswaChart from "@/components/charts/StatusMahasiswaChart"; import StatusMahasiswaPieChartPerangkatan from "@/components/chartsDashboard/StatusMahasiswaPieChartPerangkatan"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelStatusMahasiswa from "@/components/chartstable/tabelstatusmahasiswa"; export default function StatusMahasiswaDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -38,6 +39,9 @@ export default function StatusMahasiswaDetailPage() { )}

+ {/* Tabel Section */} + + {/* Information Section */}

diff --git a/components/ClientLayout.tsx b/components/ClientLayout.tsx index a78e2d5..859cc8d 100644 --- a/components/ClientLayout.tsx +++ b/components/ClientLayout.tsx @@ -42,7 +42,7 @@ export default function ClientLayout({ children }: ClientLayoutProps) { }; // Don't show navbar on the root page (login page) - const showNavbar = pathname !== '/' && user; + const showNavbar = pathname !== '/' && !isLoading; return ( ({ chart: { type: 'bar', - stacked: false, + stacked: true, toolbar: { show: true, }, @@ -43,14 +43,27 @@ export default function JenisPendaftaranChart({ }, plotOptions: { bar: { - horizontal: false, - columnWidth: '55%', + horizontal: true, + barHeight: '70%', }, }, dataLabels: { enabled: true, - formatter: function (val: number) { - return val.toString(); + formatter: function (val: number, opts: any) { + const seriesIndex = opts.seriesIndex; + const dataPointIndex = opts.dataPointIndex; + + // Hitung total untuk tahun angkatan ini + const allSeries = opts.w.config.series; + let totalForYear = 0; + allSeries.forEach((series: any) => { + totalForYear += series.data[dataPointIndex] || 0; + }); + + if (totalForYear === 0 || val === 0) return '0%'; + + const percentage = ((val / totalForYear) * 100).toFixed(0); + return percentage + '%'; }, style: { fontSize: '12px', @@ -64,22 +77,6 @@ export default function JenisPendaftaranChart({ }, xaxis: { categories: [], - title: { - text: 'Tahun Angkatan', - style: { - fontSize: '14px', - fontWeight: 'bold', - color: theme === 'dark' ? '#fff' : '#000' - } - }, - labels: { - style: { - fontSize: '12px', - colors: theme === 'dark' ? '#fff' : '#000' - } - }, - }, - yaxis: { title: { text: 'Jumlah Mahasiswa', style: { @@ -94,9 +91,25 @@ export default function JenisPendaftaranChart({ colors: theme === 'dark' ? '#fff' : '#000' } }, - min:0, + 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, }, @@ -116,10 +129,68 @@ export default function JenisPendaftaranChart({ colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4', '#F97316'], tooltip: { theme: theme === 'dark' ? 'dark' : 'light', - y: { - formatter: function (val: number) { - return val + " mahasiswa"; - } + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const tahun = w.globals.labels[dataPointIndex]; + const isDark = theme === 'dark'; + + // Hitung total untuk tahun ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const jenisPendaftaranNames = w.config.series.map((s: any) => s.name); + const jenisPendaftaranColors = w.config.colors; + + let tooltipContent = ` +
+
Angkatan ${tahun}
`; + + // Tambahkan setiap jenis pendaftaran + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + tooltipContent += ` +
+
+ ${jenisPendaftaranNames[index]} + ${value} +
`; + }); + + // Tambahkan total + tooltipContent += ` +
+
+ Total + ${total} +
+
+ `; + + return tooltipContent; } } }); @@ -135,6 +206,22 @@ export default function JenisPendaftaranChart({ }, dataLabels: { ...prev.dataLabels, + 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: { ...prev.dataLabels?.style, colors: [currentTheme === 'dark' ? '#fff' : '#000'] @@ -183,7 +270,68 @@ export default function JenisPendaftaranChart({ }, tooltip: { ...prev.tooltip, - theme: currentTheme === 'dark' ? 'dark' : 'light' + 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 jenisPendaftaranNames = w.config.series.map((s: any) => s.name); + const jenisPendaftaranColors = w.config.colors; + + let tooltipContent = ` +
+
Angkatan ${tahun}
`; + + // Tambahkan setiap jenis pendaftaran + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + tooltipContent += ` +
+
+ ${jenisPendaftaranNames[index]} + ${value} +
`; + }); + + // Tambahkan total + tooltipContent += ` +
+
+ Total + ${total} +
+
+ `; + + return tooltipContent; + } } })); }, [theme]); diff --git a/components/charts/StatusMahasiswaChart.tsx b/components/charts/StatusMahasiswaChart.tsx index fd0419f..bdda18c 100644 --- a/components/charts/StatusMahasiswaChart.tsx +++ b/components/charts/StatusMahasiswaChart.tsx @@ -36,7 +36,7 @@ export default function StatusMahasiswaChart({ const chartOptions: ApexOptions = { chart: { type: 'bar', - stacked: false, + stacked: true, toolbar: { show: true, tools: { @@ -53,14 +53,27 @@ export default function StatusMahasiswaChart({ }, plotOptions: { bar: { - horizontal: false, - columnWidth: '65%', + horizontal: true, + barHeight: '70%', }, }, dataLabels: { enabled: true, - formatter: function (val: number) { - return val.toString(); + formatter: function (val: number, opts: any) { + const seriesIndex = opts.seriesIndex; + const dataPointIndex = opts.dataPointIndex; + + // Hitung total untuk tahun angkatan ini + const allSeries = opts.w.config.series; + let totalForYear = 0; + allSeries.forEach((series: any) => { + totalForYear += series.data[dataPointIndex] || 0; + }); + + if (totalForYear === 0 || val === 0) return '0%'; + + const percentage = ((val / totalForYear) * 100).toFixed(0); + return percentage + '%'; }, style: { fontSize: '12px', @@ -74,22 +87,6 @@ export default function StatusMahasiswaChart({ }, xaxis: { categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), - title: { - text: 'Tahun Angkatan', - style: { - fontSize: '14px', - fontWeight: 'bold', - color: theme === 'dark' ? '#fff' : '#000' - } - }, - labels: { - style: { - fontSize: '12px', - colors: theme === 'dark' ? '#fff' : '#000' - } - } - }, - yaxis: { title: { text: 'Jumlah Mahasiswa', style: { @@ -104,9 +101,25 @@ export default function StatusMahasiswaChart({ colors: theme === 'dark' ? '#fff' : '#000' } }, - min:0, + 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, }, @@ -126,10 +139,68 @@ export default function StatusMahasiswaChart({ colors: ['#008FFB', '#00E396', '#FEB019', '#EF4444'], tooltip: { theme: theme === 'dark' ? 'dark' : 'light', - y: { - formatter: function (val: number) { - return val + " mahasiswa"; - } + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const tahun = w.globals.labels[dataPointIndex]; + const isDark = theme === 'dark'; + + // Hitung total untuk tahun ini + let total = 0; + series.forEach((seriesData: number[]) => { + total += seriesData[dataPointIndex] || 0; + }); + + const statusNames = ['Aktif', 'Lulus', 'Cuti', 'Non Aktif']; + const statusColors = ['#008FFB', '#00E396', '#FEB019', '#EF4444']; + + let tooltipContent = ` +
+
Angkatan ${tahun}
`; + + // Tambahkan setiap status + series.forEach((seriesData: number[], index: number) => { + const value = seriesData[dataPointIndex] || 0; + tooltipContent += ` +
+
+ ${statusNames[index]} + ${value} +
`; + }); + + // Tambahkan total + tooltipContent += ` +
+
+ Total + ${total} +
+
+ `; + + return tooltipContent; } } }; diff --git a/components/chartstable/tabelipkmahasiswa.tsx b/components/chartstable/tabelipkmahasiswa.tsx new file mode 100644 index 0000000..8ed95e0 --- /dev/null +++ b/components/chartstable/tabelipkmahasiswa.tsx @@ -0,0 +1,366 @@ +'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 MahasiswaIPK { + nim: string; + nama: string; + ipk: number; + tahun_angkatan: number; +} + +interface TabelIPKMahasiswaProps { + selectedYear: string; +} + +export default function TabelIPKMahasiswa({ selectedYear }: TabelIPKMahasiswaProps) { + 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/ipk' + : `/api/tabeldetail/ipk?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa IPK data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + + // Fungsi untuk mendapatkan icon berdasarkan ranking + const getRankingIcon = (index: number) => { + if (index === 0) return ; + if (index === 1) return ; + if (index === 2) return ; + return null; + }; + + // Hitung statistik IPK + const ipkStats = { + highest: mahasiswaData.length > 0 ? Math.max(...mahasiswaData.map(m => m.ipk)) : 0, + lowest: mahasiswaData.length > 0 ? Math.min(...mahasiswaData.map(m => m.ipk)) : 0, + average: mahasiswaData.length > 0 ? + mahasiswaData.reduce((sum, m) => sum + m.ipk, 0) / mahasiswaData.length : 0, + total: mahasiswaData.length + }; + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + Tabel IPK Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik IPK */} +
+
+
Tertinggi
+
{ipkStats.highest.toFixed(2)}
+
+
+
Terendah
+
{ipkStats.lowest.toFixed(2)}
+
+
+
Rata-rata
+
{ipkStats.average.toFixed(2)}
+
+
+
Total
+
{ipkStats.total}
+
+
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + Ranking + NIM + Nama Mahasiswa + Tahun Angkatan + IPK + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => { + const globalIndex = (currentPage - 1) * pageSize + index; + return ( + + +
+ {getRankingIcon(globalIndex)} + {globalIndex + 1} +
+
+ + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.ipk.toFixed(2)} + +
+ ); + }) + )} +
+
+
+ + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
+
+ Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
+ + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+ ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/chartstable/tabeljenisPendaftaranmahasiswa.tsx b/components/chartstable/tabeljenisPendaftaranmahasiswa.tsx new file mode 100644 index 0000000..5f163ac --- /dev/null +++ b/components/chartstable/tabeljenisPendaftaranmahasiswa.tsx @@ -0,0 +1,346 @@ +'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, GraduationCap } from "lucide-react"; + +interface MahasiswaJenisPendaftaran { + nim: string; + nama: string; + jenis_pendaftaran: string; + tahun_angkatan: number; +} + +interface TabelJenisPendaftaranMahasiswaProps { + selectedYear: string; +} + +export default function TabelJenisPendaftaranMahasiswa({ selectedYear }: TabelJenisPendaftaranMahasiswaProps) { + 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/jenis-pendaftaran' + : `/api/tabeldetail/jenis-pendaftaran?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa jenis pendaftaran data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + + // Hitung statistik berdasarkan jenis pendaftaran + const jenisPendaftaranStats = mahasiswaData.reduce((acc, mahasiswa) => { + const jenis = mahasiswa.jenis_pendaftaran || 'Tidak Diketahui'; + acc[jenis] = (acc[jenis] || 0) + 1; + return acc; + }, {} as Record); + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Jenis Pendaftaran Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik jenis pendaftaran */} +
+ {Object.entries(jenisPendaftaranStats).map(([jenis, count]) => ( + + {jenis}: {count} + + ))} + + Total: {mahasiswaData.length} + +
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Jenis Pendaftaran + + + + {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.jenis_pendaftaran || 'Tidak Diketahui'} + + + )) + )} + +
+
+ + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
+
+ Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
+ + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+ ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/chartstable/tabelkelompokkeahliamahasiswa.tsx b/components/chartstable/tabelkelompokkeahliamahasiswa.tsx new file mode 100644 index 0000000..1bf7b24 --- /dev/null +++ b/components/chartstable/tabelkelompokkeahliamahasiswa.tsx @@ -0,0 +1,360 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Loader2, Users } from "lucide-react"; + +interface MahasiswaKelompokKeahlian { + nim: string; + nama: string; + tahun_angkatan: number; + nama_kelompok_keahlian: string; +} + +interface TabelKelompokKeahlianMahasiswaProps { + selectedYear: string; +} + +export default function TabelKelompokKeahlianMahasiswa({ selectedYear }: TabelKelompokKeahlianMahasiswaProps) { + 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/kelompok-keahlian' + : `/api/tabeldetail/kelompok-keahlian?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa kelompok keahlian 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 kelompok keahlian + const kelompokKeahlianStats = mahasiswaData.reduce((acc, mahasiswa) => { + const kelompok = mahasiswa.nama_kelompok_keahlian; + acc[kelompok] = (acc[kelompok] || 0) + 1; + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_kelompok: Object.keys(kelompokKeahlianStats).length + }; + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Kelompok Keahlian Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
+
+
Total Mahasiswa
+
{stats.total}
+
+
+
Total Kelompok Keahlian
+
{stats.total_kelompok}
+
+
+ {/* Tampilkan ringkasan kelompok keahlian */} +
+ {Object.entries(kelompokKeahlianStats) + .sort(([,a], [,b]) => b - a) // Urutkan berdasarkan jumlah mahasiswa terbanyak + .map(([kelompok, count]) => ( + + {kelompok}: {count} + + ))} +
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Kelompok Keahlian + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => ( + + + {(currentPage - 1) * pageSize + index + 1} + + + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.nama_kelompok_keahlian} + + + )) + )} + +
+
+ + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
+
+ Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
+ + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+ ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/chartstable/tabelkklulustepatmahasiswa.tsx b/components/chartstable/tabelkklulustepatmahasiswa.tsx new file mode 100644 index 0000000..a5c6a69 --- /dev/null +++ b/components/chartstable/tabelkklulustepatmahasiswa.tsx @@ -0,0 +1,386 @@ +'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, GraduationCap, Trophy, Medal, Award } from "lucide-react"; + +interface MahasiswaKKLulusTepat { + nim: string; + nama: string; + tahun_angkatan: number; + nama_kelompok_keahlian: string; + semester: number; +} + +interface TabelKKLulusTepatMahasiswaProps { + selectedYear: string; +} + +export default function TabelKKLulusTepatMahasiswa({ selectedYear }: TabelKKLulusTepatMahasiswaProps) { + 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/kk-lulus-tepat' + : `/api/tabeldetail/kk-lulus-tepat?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa kelompok keahlian lulus tepat data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Fungsi untuk mendapatkan icon berdasarkan ranking semester + const getRankingIcon = (index: number) => { + if (index === 0) return ; + if (index === 1) return ; + if (index === 2) return ; + return null; + }; + + // Hitung statistik kelompok keahlian dan semester + const kelompokKeahlianStats = mahasiswaData.reduce((acc, mahasiswa) => { + const kelompok = mahasiswa.nama_kelompok_keahlian; + acc[kelompok] = (acc[kelompok] || 0) + 1; + return acc; + }, {} as Record); + + const stats = { + total: mahasiswaData.length, + total_kelompok: Object.keys(kelompokKeahlianStats).length, + fastest_semester: mahasiswaData.length > 0 ? Math.min(...mahasiswaData.map(m => m.semester)) : 0 + }; + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Mahasiswa Lulus Tepat Waktu per Kelompok Keahlian {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
+
+
Total Lulus Tepat Waktu
+
{stats.total}
+
+
+
Total Kelompok Keahlian
+
{stats.total_kelompok}
+
+
+
Semester Tercepat
+
{stats.fastest_semester}
+
+
+ {/* Tampilkan ringkasan kelompok keahlian */} +
+ {Object.entries(kelompokKeahlianStats) + .sort(([,a], [,b]) => b - a) // Urutkan berdasarkan jumlah mahasiswa terbanyak + .map(([kelompok, count]) => ( + + {kelompok}: {count} + + ))} +
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + Ranking + NIM + Nama Mahasiswa + Tahun Angkatan + Kelompok Keahlian + Semester Lulus + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => { + const globalIndex = (currentPage - 1) * pageSize + index; + return ( + + +
+ {getRankingIcon(globalIndex)} + {globalIndex + 1} +
+
+ + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.nama_kelompok_keahlian} + + + + {mahasiswa.semester} 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; // 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/tabellulustepatwaktumahasiswa.tsx b/components/chartstable/tabellulustepatwaktumahasiswa.tsx new file mode 100644 index 0000000..0348e79 --- /dev/null +++ b/components/chartstable/tabellulustepatwaktumahasiswa.tsx @@ -0,0 +1,371 @@ +'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, Clock, Trophy, Medal, Award } from "lucide-react"; + +interface MahasiswaLulusTepatWaktu { + nim: string; + nama: string; + tahun_angkatan: number; + ipk: number; + semester: number; +} + +interface TabelLulusTepatWaktuMahasiswaProps { + selectedYear: string; +} + +export default function TabelLulusTepatWaktuMahasiswa({ selectedYear }: TabelLulusTepatWaktuMahasiswaProps) { + 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/lulus-tepat-waktu' + : `/api/tabeldetail/lulus-tepat-waktu?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa lulus tepat waktu data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Fungsi untuk mendapatkan icon berdasarkan ranking + const getRankingIcon = (index: number) => { + if (index === 0) return ; + if (index === 1) return ; + if (index === 2) return ; + return null; + }; + + // Hitung statistik IPK dan semester + const stats = { + highest_ipk: mahasiswaData.length > 0 ? Math.max(...mahasiswaData.map(m => m.ipk)) : 0, + lowest_ipk: mahasiswaData.length > 0 ? Math.min(...mahasiswaData.map(m => m.ipk)) : 0, + average_ipk: mahasiswaData.length > 0 ? + mahasiswaData.reduce((sum, m) => sum + m.ipk, 0) / mahasiswaData.length : 0, + total: mahasiswaData.length + }; + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Mahasiswa Lulus Tepat Waktu {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
+
+
IPK Tertinggi
+
{stats.highest_ipk.toFixed(2)}
+
+
+
IPK Terendah
+
{stats.lowest_ipk.toFixed(2)}
+
+
+
Rata-rata IPK
+
{stats.average_ipk.toFixed(2)}
+
+
+
Total
+
{stats.total}
+
+
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + Ranking + NIM + Nama Mahasiswa + Tahun Angkatan + IPK + Semester Lulus + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((mahasiswa, index) => { + const globalIndex = (currentPage - 1) * pageSize + index; + return ( + + +
+ {getRankingIcon(globalIndex)} + {globalIndex + 1} +
+
+ + {mahasiswa.nim} + + + {mahasiswa.nama} + + + {mahasiswa.tahun_angkatan} + + + {mahasiswa.ipk.toFixed(2)} + + + {mahasiswa.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; // 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/tabelmasastudimahasiswa.tsx b/components/chartstable/tabelmasastudimahasiswa.tsx new file mode 100644 index 0000000..95fc863 --- /dev/null +++ b/components/chartstable/tabelmasastudimahasiswa.tsx @@ -0,0 +1,355 @@ +'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, BookOpen } from "lucide-react"; + +interface MahasiswaMasaStudi { + nim: string; + nama: string; + tahun_angkatan: number; + semester: number; + status_kuliah: string; + masa_studi_tahun: number; +} + +interface TabelMasaStudiMahasiswaProps { + selectedYear: string; +} + +export default function TabelMasaStudiMahasiswa({ selectedYear }: TabelMasaStudiMahasiswaProps) { + 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/masa-studi' + : `/api/tabeldetail/masa-studi?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa masa studi 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 masa studi + const stats = { + highest_masa_studi: mahasiswaData.length > 0 ? Math.max(...mahasiswaData.map(m => m.masa_studi_tahun)) : 0, + lowest_masa_studi: mahasiswaData.length > 0 ? Math.min(...mahasiswaData.map(m => m.masa_studi_tahun)) : 0, + average_masa_studi: mahasiswaData.length > 0 ? + mahasiswaData.reduce((sum, m) => sum + m.masa_studi_tahun, 0) / mahasiswaData.length : 0, + total: mahasiswaData.length + }; + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + + Tabel Masa Studi Mahasiswa Lulus {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
+
+
Masa Studi Terlama
+
{stats.highest_masa_studi} tahun
+
+
+
Masa Studi Tercepat
+
{stats.lowest_masa_studi} tahun
+
+
+
Rata-rata Masa Studi
+
{stats.average_masa_studi.toFixed(1)} tahun
+
+
+
Total Mahasiswa Lulus
+
{stats.total}
+
+
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Masa Studi (Tahun) + + + + + {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.masa_studi_tahun} tahun + + + )) + )} + +
+
+ + {/* 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; + } +} \ No newline at end of file diff --git a/components/chartstable/tabelstatusmahasiswa.tsx b/components/chartstable/tabelstatusmahasiswa.tsx new file mode 100644 index 0000000..00efccd --- /dev/null +++ b/components/chartstable/tabelstatusmahasiswa.tsx @@ -0,0 +1,365 @@ +'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 { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Loader2 } from "lucide-react"; + +interface MahasiswaStatus { + nim: string; + nama: string; + status_kuliah: string; + tahun_angkatan: number; +} + +interface TabelStatusMahasiswaProps { + selectedYear: string; +} + +export default function TabelStatusMahasiswa({ selectedYear }: TabelStatusMahasiswaProps) { + 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' + : `/api/tabeldetail?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url, { + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error('Failed to fetch mahasiswa data'); + } + + const data = await response.json(); + setMahasiswaData(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Update paginated data when data or pagination settings change + useEffect(() => { + paginateData(); + }, [mahasiswaData, currentPage, pageSize]); + + // Paginate data + const paginateData = () => { + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + setPaginatedData(mahasiswaData.slice(startIndex, endIndex)); + }; + + // Get total number of pages + const getTotalPages = () => { + return Math.ceil(mahasiswaData.length / pageSize); + }; + + // Handle page change + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (size: string) => { + setPageSize(Number(size)); + setCurrentPage(1); // Reset to first page when changing page size + }; + + // Fungsi untuk mendapatkan styling badge berdasarkan status (sama seperti data-table-mahasiswa) + const getStatusBadgeStyle = (status: string) => { + switch (status) { + case "Aktif": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"; + case "Cuti": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"; + case "Lulus": + return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"; + case "Non Aktif": + return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"; + } + }; + + // Hitung statistik berdasarkan status + const statusStats = mahasiswaData.reduce((acc, mahasiswa) => { + const status = mahasiswa.status_kuliah; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + Tabel Status Mahasiswa {selectedYear !== 'all' ? `Angkatan ${selectedYear}` : 'Semua Angkatan'} + + {/* Tampilkan ringkasan statistik */} +
+ {Object.entries(statusStats).map(([status, count]) => ( + + {status}: {count} + + ))} + + Total: {mahasiswaData.length} + +
+
+ + {/* Show entries selector */} +
+ Show + + entries +
+ +
+ + + + No + NIM + Nama Mahasiswa + Tahun Angkatan + Status Kuliah + + + + {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.status_kuliah} + + + + )) + )} + +
+
+ + {/* Pagination info and controls */} + {!loading && !error && mahasiswaData.length > 0 && ( +
+
+ Showing {getDisplayRange().start} to {getDisplayRange().end} of {mahasiswaData.length} entries +
+ + + + handlePageChange(Math.max(1, currentPage - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} +
+
+ ); + + // Calculate the range of entries being displayed + function getDisplayRange() { + if (mahasiswaData.length === 0) return { start: 0, end: 0 }; + + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, mahasiswaData.length); + + return { start, end }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 1 + + + ); + + // Show ellipsis if needed + if (currentPage > 3) { + items.push( + + + + ); + } + + // Show pages around current page + for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { + if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {i} + + + ); + } + + // Show ellipsis if needed + if (currentPage < totalPages - 2) { + items.push( + + + + ); + } + + // Always show last page if there's more than one page + if (totalPages > 1) { + items.push( + + handlePageChange(totalPages)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } +} diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index b54614f..1ebad74 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -26,6 +26,8 @@ interface UserData { const Navbar = () => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [isVisible, setIsVisible] = useState(true); + const [lastScrollY, setLastScrollY] = useState(0); const { showSuccess, showError } = useToast (); const router = useRouter(); @@ -34,6 +36,29 @@ const Navbar = () => { checkUserSession(); }, []); + // Handle scroll behavior + useEffect(() => { + const controlNavbar = () => { + if (typeof window !== 'undefined') { + if (window.scrollY > lastScrollY && window.scrollY > 100) { + // Scrolling down & past 100px + setIsVisible(false); + } else { + // Scrolling up + setIsVisible(true); + } + setLastScrollY(window.scrollY); + } + }; + + if (typeof window !== 'undefined') { + window.addEventListener('scroll', controlNavbar); + return () => { + window.removeEventListener('scroll', controlNavbar); + }; + } + }, [lastScrollY]); + const checkUserSession = async () => { try { const response = await fetch('/api/auth/user'); @@ -89,7 +114,9 @@ const Navbar = () => { } return ( -
+
{/* Logo */}
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..fd3a406 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants }