From 8363f5e3b2ddc0d5ad987812ddac04f30186b4ad Mon Sep 17 00:00:00 2001 From: Randa Firman Putra Date: Tue, 15 Jul 2025 22:58:45 +0700 Subject: [PATCH] Add Visualisasi Masa Studi --- app/api/mahasiswa/masa-studi-status/route.ts | 95 +++++++ app/page.tsx | 4 +- app/visualisasi/status/page.tsx | 5 + components/charts/IpkStatusChart.tsx | 2 +- components/charts/MasaStudiChart.tsx | 272 +++++++++++++++++++ 5 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 app/api/mahasiswa/masa-studi-status/route.ts create mode 100644 components/charts/MasaStudiChart.tsx diff --git a/app/api/mahasiswa/masa-studi-status/route.ts b/app/api/mahasiswa/masa-studi-status/route.ts new file mode 100644 index 0000000..6f9ddd8 --- /dev/null +++ b/app/api/mahasiswa/masa-studi-status/route.ts @@ -0,0 +1,95 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MasaStudiData { + tahun_angkatan: number; + status_kuliah: string; + rata_rata_masa_studi_tahun: number; +} + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const tahun_angkatan = searchParams.get('tahun_angkatan'); + const status_kuliah = searchParams.get('status_kuliah'); + + let query = supabase + .from('mahasiswa') + .select('tahun_angkatan, status_kuliah, semester') + .not('semester', 'is', null); + + if (tahun_angkatan && tahun_angkatan !== 'all') { + query = query.eq('tahun_angkatan', tahun_angkatan); + } + if (status_kuliah && status_kuliah !== 'all') { + query = query.eq('status_kuliah', status_kuliah); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching masa studi data:', error); + return NextResponse.json( + { error: 'Failed to fetch masa studi data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } + + // Group by tahun_angkatan and status_kuliah, calculate average semester/2 + const groupedData = data.reduce((acc, item) => { + const tahun = item.tahun_angkatan; + const status = item.status_kuliah; + const key = `${tahun}|${status}`; + if (!acc[key]) { + acc[key] = { sum: 0, count: 0, tahun_angkatan: tahun, status_kuliah: status }; + } + acc[key].sum += item.semester || 0; + acc[key].count += 1; + return acc; + }, {} as Record); + + // Convert to final format + const results: MasaStudiData[] = Object.values(groupedData).map((data) => ({ + tahun_angkatan: data.tahun_angkatan, + status_kuliah: data.status_kuliah, + rata_rata_masa_studi_tahun: Math.round(((data.sum / data.count) / 2) * 10) / 10, + })); + + // Sort by tahun_angkatan, status_kuliah + results.sort((a, b) => { + if (a.tahun_angkatan !== b.tahun_angkatan) { + return a.tahun_angkatan - b.tahun_angkatan; + } + return a.status_kuliah.localeCompare(b.status_kuliah); + }); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching masa studi data:', error); + return NextResponse.json( + { error: 'Failed to fetch masa studi data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 97a3001..b78e953 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -192,7 +192,7 @@ export default function DashboardPage() {
Pemerintah: {mahasiswaData.total_mahasiswa_beasiswa_pemerintah} Non-Pemerintah: {mahasiswaData.total_mahasiswa_beasiswa_non_pemerintah} -
+ @@ -210,6 +210,6 @@ export default function DashboardPage() { ) } - + ); } diff --git a/app/visualisasi/status/page.tsx b/app/visualisasi/status/page.tsx index 7797fe9..f7fae34 100644 --- a/app/visualisasi/status/page.tsx +++ b/app/visualisasi/status/page.tsx @@ -9,6 +9,7 @@ import JenisPendaftaranStatusChart from "@/components/charts/JenisPendaftaranSta import JenisPendaftaranStatusPieChart from "@/components/charts/JenisPendaftaranStatusPieChart"; import AsalDaerahStatusChart from '@/components/charts/AsalDaerahStatusChart'; import IpkStatusChart from '@/components/charts/IpkStatusChart'; +import MasaStudiChart from '@/components/charts/MasaStudiChart'; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export default function StatusMahasiswaPage() { @@ -51,6 +52,10 @@ export default function StatusMahasiswaPage() { selectedYear={selectedYear} selectedStatus={selectedStatus} /> + import('react-apexcharts'), { ssr: false }); + +interface MasaStudiData { + tahun_angkatan: number; + status_kuliah: string; + rata_rata_masa_studi_tahun: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function MasaStudiChart({ selectedYear, selectedStatus }: Props) { + const { theme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + let url = '/api/mahasiswa/masa-studi-status'; + const params = []; + if (selectedYear && selectedYear !== 'all') params.push(`tahun_angkatan=${selectedYear}`); + if (selectedStatus && selectedStatus !== 'all') params.push(`status_kuliah=${encodeURIComponent(selectedStatus)}`); + if (params.length > 0) url += '?' + params.join('&'); + 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'); + } + // Sort data by tahun_angkatan + const sortedData = result.sort((a: MasaStudiData, b: MasaStudiData) => a.tahun_angkatan - b.tahun_angkatan); + setData(sortedData); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [selectedYear, selectedStatus]); + + // Chart options and series + const chartOptions: ApexOptions = { + chart: { + type: selectedYear === 'all' ? 'line' : 'bar', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + zoom: { + enabled: true, + type: 'x', + autoScaleYaxis: true + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 4, + }, + }, + stroke: { + curve: 'straight', + width: 3, + lineCap: 'round' + }, + markers: { + size: 5, + strokeWidth: 2, + strokeColors: theme === 'dark' ? ['#fff'] : ['#10B981'], + colors: ['#10B981'], + hover: { + size: 7 + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toFixed(1); + }, + style: { + fontSize: '14px', + fontWeight: 'bold', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + background: { + enabled: false + }, + offsetY: -10 + }, + xaxis: { + categories: data.map(item => item.tahun_angkatan.toString()), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Rata-rata Masa Studi (tahun)', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + min: 0, + max: 8, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + formatter: function (val: number) { + return val.toFixed(1); + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + }, + colors: ['#10B981'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(1); + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }; + + const series = [{ + name: 'Rata-rata Masa Studi (tahun)', + data: data.map(item => item.rata_rata_masa_studi_tahun) + }]; + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Rata-rata Masa Studi Mahasiswa {selectedStatus !== 'all' ? selectedStatus : ''} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file