diff --git a/app/api/mahasiswa/kk-dashboard-tepat/route.ts b/app/api/mahasiswa/kk-dashboard-tepat/route.ts new file mode 100644 index 0000000..0edc8df --- /dev/null +++ b/app/api/mahasiswa/kk-dashboard-tepat/route.ts @@ -0,0 +1,97 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface KelompokKeahlianLulusTepat { + nama_kelompok: string; + jumlah_lulusan_tercepat: number; +} + +export async function GET(request: Request) { + try { + // Get all lulus students with their kelompok keahlian + const { data, error } = await supabase + .from('mahasiswa') + .select(` + kelompok_keahlian!inner( + nama_kelompok + ), + semester, + status_kuliah + `) + .eq('status_kuliah', 'Lulus'); + + if (error) { + console.error('Error fetching kelompok keahlian lulus tepat:', error); + return NextResponse.json( + { error: 'Failed to fetch kelompok keahlian lulus tepat data' }, + { + status: 500, + 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', + }, + } + ); + } + + // Process data to calculate percentages + const groupedData = data.reduce((acc, item: any) => { + const nama_kelompok = item.kelompok_keahlian?.nama_kelompok; + if (!nama_kelompok) return acc; + + if (!acc[nama_kelompok]) { + acc[nama_kelompok] = { + total_lulus: 0, + lulus_tepat: 0 + }; + } + + acc[nama_kelompok].total_lulus += 1; + + // Check if lulus tepat waktu (semester <= 8) + if (item.semester <= 8) { + acc[nama_kelompok].lulus_tepat += 1; + } + + return acc; + }, {} as Record); + + // Convert to final format (without percentage) and sort by count DESC then name ASC + const results: KelompokKeahlianLulusTepat[] = Object.entries(groupedData) + .map(([nama_kelompok, counts]) => ({ + nama_kelompok, + jumlah_lulusan_tercepat: counts.lulus_tepat, + })) + .sort((a, b) => { + if (b.jumlah_lulusan_tercepat !== a.jumlah_lulusan_tercepat) { + return b.jumlah_lulusan_tercepat - a.jumlah_lulusan_tercepat; + } + return a.nama_kelompok.localeCompare(b.nama_kelompok); + }); + + 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 kelompok keahlian lulus tepat:', error); + return NextResponse.json( + { error: 'Failed to fetch kelompok keahlian lulus tepat data' }, + { + status: 500, + 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', + }, + } + ); + } +} diff --git a/app/api/mahasiswa/kk-dashboard/route.ts b/app/api/mahasiswa/kk-dashboard/route.ts new file mode 100644 index 0000000..9b87761 --- /dev/null +++ b/app/api/mahasiswa/kk-dashboard/route.ts @@ -0,0 +1,91 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface KelompokKeahlianStatus { + tahun_angkatan: number; + nama_kelompok: string; + jumlah_mahasiswa: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + try { + let query = supabase + .from('mahasiswa') + .select('tahun_angkatan, id_kelompok_keahlian, kelompok_keahlian!inner(id_kk, nama_kelompok)'); + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching kelompok keahlian status:', error); + return NextResponse.json( + { error: 'Failed to fetch kelompok keahlian status data' }, + { + status: 500, + 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', + }, + } + ); + } + + // Group by tahun_angkatan, nama_kelompok + const groupedData = data.reduce((acc, item: any) => { + const tahun_angkatan = item.tahun_angkatan; + const nama_kelompok = item.kelompok_keahlian?.nama_kelompok; + const key = `${tahun_angkatan}-${nama_kelompok}`; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {} as Record); + + // Convert to final format and sort + const results: KelompokKeahlianStatus[] = Object.entries(groupedData) + .map(([key, jumlah_mahasiswa]) => { + const [tahun_angkatan, nama_kelompok] = key.split('-'); + return { + tahun_angkatan: parseInt(tahun_angkatan), + nama_kelompok, + jumlah_mahasiswa + }; + }) + .sort((a, b) => { + // Sort by tahun_angkatan ASC, nama_kelompok ASC + if (a.tahun_angkatan !== b.tahun_angkatan) { + return a.tahun_angkatan - b.tahun_angkatan; + } + return a.nama_kelompok.localeCompare(b.nama_kelompok); + }); + + 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 kelompok keahlian status:', error); + return NextResponse.json( + { error: 'Failed to fetch kelompok keahlian status data' }, + { + status: 500, + 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', + }, + } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/masa-studi-aktif/route.ts b/app/api/mahasiswa/masa-studi-aktif/route.ts new file mode 100644 index 0000000..fc60060 --- /dev/null +++ b/app/api/mahasiswa/masa-studi-aktif/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface MasaStudiAktifData { + tahun_angkatan: number; + rata_rata_masa_studi_aktif_tahun: number; + rata_rata_masa_studi_lulus_tahun: number; +} + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const tahun_angkatan = searchParams.get('tahun_angkatan'); + + let query = supabase + .from('mahasiswa') + .select('tahun_angkatan, status_kuliah, semester') + .not('semester', 'is', null) + .in('status_kuliah', ['Aktif', 'Lulus']); + + if (tahun_angkatan && tahun_angkatan !== 'all') { + query = query.eq('tahun_angkatan', tahun_angkatan); + } + + const { data, error } = await query; + + if (error) { + console.error('Error fetching masa studi aktif data:', error); + return NextResponse.json( + { error: 'Failed to fetch masa studi aktif 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, separate active and graduated students + const groupedData = data.reduce((acc, item) => { + const tahun = item.tahun_angkatan; + if (!acc[tahun]) { + acc[tahun] = { + tahun_angkatan: tahun, + aktif: { sum: 0, count: 0 }, + lulus: { sum: 0, count: 0 } + }; + } + + if (item.status_kuliah === 'Aktif') { + acc[tahun].aktif.sum += item.semester || 0; + acc[tahun].aktif.count += 1; + } else if (item.status_kuliah === 'Lulus') { + acc[tahun].lulus.sum += item.semester || 0; + acc[tahun].lulus.count += 1; + } + + return acc; + }, {} as Record); + + // Convert to final format + const results: MasaStudiAktifData[] = Object.values(groupedData).map((data) => ({ + tahun_angkatan: data.tahun_angkatan, + rata_rata_masa_studi_aktif_tahun: data.aktif.count > 0 + ? Math.round(((data.aktif.sum / data.aktif.count) / 2) * 10) / 10 + : 0, + rata_rata_masa_studi_lulus_tahun: data.lulus.count > 0 + ? Math.round(((data.lulus.sum / data.lulus.count) / 2) * 10) / 10 + : 0, + })); + + // Sort by tahun_angkatan + results.sort((a, b) => a.tahun_angkatan - b.tahun_angkatan); + + 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 aktif data:', error); + return NextResponse.json( + { error: 'Failed to fetch masa studi aktif data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 79ff7a7..6a95c2c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -10,6 +10,10 @@ import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; import JenisPendaftaranPerAngkatanChart from "@/components/charts/JenisPendaftaranPerAngkatanChart"; import AsalDaerahPerAngkatanChart from "@/components/charts/AsalDaerahPerAngkatanChart"; import StatusMahasiswaChart from "@/components/charts/StatusMahasiswaChart"; +import KelompokKeahlianStatusChart from "@/components/chartsDashboard/kkdashboardchart"; +import KelompokKeahlianLulusTepatPieChart from "@/components/chartsDashboard/kkdashboardtepatpiechart"; +import MasaStudiAktifChart from "@/components/chartsDashboard/masastudiaktifchart"; +import MasaStudiLulusChart from "@/components/chartsDashboard/masastudiluluschart"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export default function TotalMahasiswaPage() { @@ -40,13 +44,18 @@ export default function TotalMahasiswaPage() { + + + + ) : (
+
)} diff --git a/components/chartsDashboard/kkdashboardchart.tsx b/components/chartsDashboard/kkdashboardchart.tsx new file mode 100644 index 0000000..16ae973 --- /dev/null +++ b/components/chartsDashboard/kkdashboardchart.tsx @@ -0,0 +1,221 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface KelompokKeahlianStatusData { + tahun_angkatan: number; + nama_kelompok: string; + jumlah_mahasiswa: number; +} + +interface Props { + selectedYear: string; +} + +export default function KelompokKeahlianStatusChart({ selectedYear }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const response = await fetch( + `/api/mahasiswa/kk-dashboard?tahun_angkatan=${selectedYear}` + ); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = await response.json(); + const sortedData = result.sort((a: KelompokKeahlianStatusData, b: KelompokKeahlianStatusData) => + a.nama_kelompok.localeCompare(b.nama_kelompok) + ); + setData(sortedData); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [selectedYear]); + + // Get unique tahun_angkatan (series) + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => Number(a) - Number(b)); + // Get unique kelompok keahlian (x axis) + const kelompokList = [...new Set(data.map(item => item.nama_kelompok))]; + + // Process data for series + const processSeriesData = () => { + return kelompokList.map(kelompok => { + const seriesData = years.map(tahun => { + const found = data.find(item => item.tahun_angkatan === tahun && item.nama_kelompok === kelompok); + return found ? found.jumlah_mahasiswa : 0; + }); + return { + name: kelompok, + data: seriesData + }; + }); + }; + + const series = processSeriesData(); + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: true, + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '55%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'], + }, + xaxis: { + categories: years.map(y => y.toString()), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + }, + fill: { + opacity: 1, + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + colors: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0', '#8B5CF6', '#EC4899', '#06B6D4', '#F97316'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + ' mahasiswa'; + } + } + } + }; + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Kelompok Keahlian Mahasiswa + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/chartsDashboard/kkdashboardtepatpiechart.tsx b/components/chartsDashboard/kkdashboardtepatpiechart.tsx new file mode 100644 index 0000000..60b5699 --- /dev/null +++ b/components/chartsDashboard/kkdashboardtepatpiechart.tsx @@ -0,0 +1,179 @@ +'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"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface KelompokKeahlianLulusTepatData { + nama_kelompok: string; + jumlah_lulusan_tercepat: number; +} + +export default function KelompokKeahlianLulusTepatPieChart() { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/kk-dashboard-tepat`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4', '#F97316'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000', + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const kelompokKeahlian = data.map(item => item.nama_kelompok); + const jumlahData = data.map(item => item.jumlah_lulusan_tercepat); + + return { + series: jumlahData, + labels: kelompokKeahlian + }; + }; + + const { series, labels } = processSeriesData(); + const totalLulusTepat = data.reduce((sum, item) => sum + item.jumlah_lulusan_tercepat, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Kelompok Keahlian Lulusan Tepat Waktu + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} diff --git a/components/chartsDashboard/masastudiaktifchart.tsx b/components/chartsDashboard/masastudiaktifchart.tsx new file mode 100644 index 0000000..4d3f101 --- /dev/null +++ b/components/chartsDashboard/masastudiaktifchart.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface MasaStudiAktifData { + tahun_angkatan: number; + rata_rata_masa_studi_aktif_tahun: number; + rata_rata_masa_studi_lulus_tahun: number; +} + +interface Props { + selectedYear: string; +} + +export default function MasaStudiAktifChart({ selectedYear }: 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-aktif'; + if (selectedYear && selectedYear !== 'all') { + url += `?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'); + } + // Sort data by tahun_angkatan + const sortedData = result.sort((a: MasaStudiAktifData, b: MasaStudiAktifData) => 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]); + + // 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'] : ['#3B82F6'], + colors: ['#3B82F6'], + 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: ['#3B82F6'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(1) + ' tahun'; + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }; + + const series = [{ + name: 'Rata-rata Masa Studi Aktif (tahun)', + data: data.map(item => item.rata_rata_masa_studi_aktif_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 Aktif + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} diff --git a/components/chartsDashboard/masastudiluluschart.tsx b/components/chartsDashboard/masastudiluluschart.tsx new file mode 100644 index 0000000..b213b87 --- /dev/null +++ b/components/chartsDashboard/masastudiluluschart.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface MasaStudiAktifData { + tahun_angkatan: number; + rata_rata_masa_studi_aktif_tahun: number; + rata_rata_masa_studi_lulus_tahun: number; +} + +interface Props { + selectedYear: string; +} + +export default function MasaStudiLulusChart({ selectedYear }: 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-aktif'; + if (selectedYear && selectedYear !== 'all') { + url += `?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'); + } + // Sort data by tahun_angkatan + const sortedData = result.sort((a: MasaStudiAktifData, b: MasaStudiAktifData) => 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]); + + // 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'] : ['#3B82F6'], + colors: ['#3B82F6'], + 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: ['#3B82F6'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(1) + ' tahun'; + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }; + + const series = [{ + name: 'Rata-rata Masa Studi Lulus (tahun)', + data: data.map(item => item.rata_rata_masa_studi_lulus_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 Lulus + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +}