diff --git a/app/api/mahasiswa/nama-beasiswa-dashboard/route.ts b/app/api/mahasiswa/nama-beasiswa-dashboard/route.ts new file mode 100644 index 0000000..13a8860 --- /dev/null +++ b/app/api/mahasiswa/nama-beasiswa-dashboard/route.ts @@ -0,0 +1,77 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + + let query = supabase + .from('beasiswa_mahasiswa') + .select(` + nama_beasiswa, + jenis_beasiswa, + mahasiswa!inner( + tahun_angkatan + ) + `); + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query = query.eq('mahasiswa.tahun_angkatan', tahunAngkatan); + } + + const { data, error } = await query; + + if (error) { + console.error('Supabase error:', error); + return NextResponse.json( + { error: 'Database error' }, + { status: 500 } + ); + } + + // Group and count the data in JavaScript + const groupedData = data.reduce((acc: any[], row: any) => { + const tahunAngkatanValue = row.mahasiswa?.tahun_angkatan; + const namaBeasiswa = row.nama_beasiswa; + + if (!namaBeasiswa || !tahunAngkatanValue) { + return acc; + } + + const existingGroup = acc.find( + (item: any) => + item.tahun_angkatan === tahunAngkatanValue && + item.nama_beasiswa === namaBeasiswa + ); + + if (existingGroup) { + existingGroup.jumlah_nama_beasiswa++; + } else { + acc.push({ + tahun_angkatan: tahunAngkatanValue, + nama_beasiswa: namaBeasiswa, + jumlah_nama_beasiswa: 1 + }); + } + + return acc; + }, []); + + // Sort the results by tahun_angkatan ascending (as expected by component) + const sortedData = groupedData.sort((a: any, b: any) => { + if (a.tahun_angkatan !== b.tahun_angkatan) { + return a.tahun_angkatan - b.tahun_angkatan; + } + return a.nama_beasiswa.localeCompare(b.nama_beasiswa); + }); + + return NextResponse.json(sortedData); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/mahasiswa/provinsi-mahasiswa/route.ts b/app/api/mahasiswa/provinsi-mahasiswa/route.ts new file mode 100644 index 0000000..9293bd0 --- /dev/null +++ b/app/api/mahasiswa/provinsi-mahasiswa/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +export async function GET(request: Request) { + try { + // Get all mahasiswa data and process in JavaScript + const { data: mahasiswaData, error } = await supabase + .from('mahasiswa') + .select('provinsi'); + + if (error) { + console.error('Supabase error:', error); + return NextResponse.json( + { error: 'Database error' }, + { status: 500 } + ); + } + + // Process the data in JavaScript + let kalimantanBarat = 0; + let luarKalimantanBarat = 0; + + mahasiswaData.forEach((mahasiswa) => { + const provinsi = mahasiswa.provinsi?.toLowerCase() || ''; + + if (provinsi.includes('kalimantan barat') || provinsi.includes('kalbar')) { + kalimantanBarat++; + } else if (mahasiswa.provinsi) { + luarKalimantanBarat++; + } + }); + + // Transform the data to match the expected format + const result = [ + { + provinsi: 'Kalimantan Barat', + jumlah_mahasiswa: kalimantanBarat + }, + { + provinsi: 'Luar Kalimantan Barat', + jumlah_mahasiswa: luarKalimantanBarat + } + ]; + + return NextResponse.json(result, { + 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 provinsi data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/mahasiswa/tingkat-prestasi-dashboard/route.ts b/app/api/mahasiswa/tingkat-prestasi-dashboard/route.ts new file mode 100644 index 0000000..270bcd1 --- /dev/null +++ b/app/api/mahasiswa/tingkat-prestasi-dashboard/route.ts @@ -0,0 +1,91 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + + // Build query based on parameters + let query = supabase + .from('mahasiswa') + .select(` + tahun_angkatan, + prestasi_mahasiswa( + tingkat_prestasi, + jenis_prestasi + ) + `); + + // Add tahun angkatan filter if provided and not 'all' + if (tahunAngkatan && tahunAngkatan !== 'null' && tahunAngkatan !== 'undefined' && tahunAngkatan !== 'all') { + query = query.eq('tahun_angkatan', parseInt(tahunAngkatan)); + } + + const { data, error } = await query; + + if (error) { + console.error('Supabase error:', error); + return NextResponse.json( + { error: 'Database error' }, + { status: 500 } + ); + } + + // Group and count the data in JavaScript + const groupedData = data.reduce((acc: any[], row: any) => { + const tahunAngkatan = row.tahun_angkatan; + + // Handle array of prestasi_mahasiswa + const prestasiArray = Array.isArray(row.prestasi_mahasiswa) ? row.prestasi_mahasiswa : [row.prestasi_mahasiswa]; + + if (!tahunAngkatan) { + return acc; + } + + // Process each prestasi in the array + prestasiArray.forEach((prestasi: any) => { + if (!prestasi || !prestasi.tingkat_prestasi) { + return; + } + + const tingkatPrestasi = prestasi.tingkat_prestasi; + + const existingGroup = acc.find( + (item: any) => + item.tahun_angkatan === tahunAngkatan && + item.tingkat_prestasi === tingkatPrestasi + ); + + if (existingGroup) { + existingGroup.tingkat_mahasiswa_prestasi++; + } else { + const newGroup = { + tahun_angkatan: tahunAngkatan, + tingkat_prestasi: tingkatPrestasi, + tingkat_mahasiswa_prestasi: 1 + }; + acc.push(newGroup); + } + }); + + return acc; + }, []); + + // Sort the results + const sortedData = groupedData.sort((a: any, b: any) => { + if (a.tahun_angkatan !== b.tahun_angkatan) { + return b.tahun_angkatan - a.tahun_angkatan; + } + return a.tingkat_prestasi.localeCompare(b.tingkat_prestasi); + }); + + return NextResponse.json(sortedData); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 6a95c2c..5b6937b 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -14,6 +14,9 @@ import KelompokKeahlianStatusChart from "@/components/chartsDashboard/kkdashboar import KelompokKeahlianLulusTepatPieChart from "@/components/chartsDashboard/kkdashboardtepatpiechart"; import MasaStudiAktifChart from "@/components/chartsDashboard/masastudiaktifchart"; import MasaStudiLulusChart from "@/components/chartsDashboard/masastudiluluschart"; +import NamaBeasiswaChart from "@/components/chartsDashboard/NamaBeasiswaDashChart"; +import TingkatPrestasiChart from "@/components/chartsDashboard/TingkatPrestasiDashChart"; +import ProvinsiMahasiswaChart from "@/components/chartsDashboard/ProvinsiMahasiswaPieChart"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export default function TotalMahasiswaPage() { @@ -39,23 +42,56 @@ export default function TotalMahasiswaPage() { {selectedYear === "all" ? ( -
- - - - - - - - - +
+ {/* Overview Section */} +
+ + +
+ + {/* Academic Performance Section */} +
+ + +
+ + {/* Study Duration Section */} +
+ + +
+ + {/* Expertise & Achievement Section */} +
+ + +
+ + {/* Scholarship & Achievement Section */} +
+ + +
+ + {/* Demographics Section */} +
+ + +
) : ( -
- - - - +
+ {/* Overview Section */} +
+ + +
+ + {/* Demographics Section */} +
+ + +
)}
diff --git a/components/chartsDashboard/NamaBeasiswaDashChart.tsx b/components/chartsDashboard/NamaBeasiswaDashChart.tsx new file mode 100644 index 0000000..e88777d --- /dev/null +++ b/components/chartsDashboard/NamaBeasiswaDashChart.tsx @@ -0,0 +1,265 @@ +'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 NamaBeasiswaData { + tahun_angkatan: number; + nama_beasiswa: string; + jumlah_nama_beasiswa: number; +} + +interface Props { + selectedYear: string; +} + +export default function NamaBeasiswaDashChart({ 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); + setError(null); + + const url = `/api/mahasiswa/nama-beasiswa-dashboard?tahunAngkatan=${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: NamaBeasiswaData, b: NamaBeasiswaData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + const beasiswaTypes = [...new Set(data.map(item => item.nama_beasiswa))].sort(); + + return beasiswaTypes.map(beasiswa => ({ + name: beasiswa, + data: years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.nama_beasiswa === beasiswa); + return item ? item.jumlah_nama_beasiswa : 0; + }) + })); + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + 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: [...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' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + tickAmount: 5, + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + 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' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#EC4899', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Nama Beasiswa Dashboard + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} diff --git a/components/chartsDashboard/ProvinsiMahasiswaPieChart.tsx b/components/chartsDashboard/ProvinsiMahasiswaPieChart.tsx new file mode 100644 index 0000000..8432c4b --- /dev/null +++ b/components/chartsDashboard/ProvinsiMahasiswaPieChart.tsx @@ -0,0 +1,185 @@ +'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 ProvinsiMahasiswaData { + provinsi: string; + jumlah_mahasiswa: number; +} + +export default function ProvinsiMahasiswaPieChart() { + 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/provinsi-mahasiswa`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + } 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', + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels: data.map(item => item.provinsi), + colors: ['#3B82F6', '#10B981'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + vertical: 5, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontWeight: 'bold', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + offsetY: -10, + dropShadow: { + enabled: true, + opacity: 0.3, + blur: 3, + color: '#000', + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + plotOptions: { + pie: { + donut: { + size: '0%', + }, + offsetY: 0, + }, + }, + stroke: { + width: 2, + colors: [theme === 'dark' ? '#0F172B' : '#fff'], + }, + states: { + hover: { + filter: { + type: 'darken', + }, + }, + }, + }; + + const series = data.map(item => item.jumlah_mahasiswa); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah_mahasiswa, 0); + + return ( + + + + Asal Provinsi Mahasiswa + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} diff --git a/components/chartsDashboard/TingkatPrestasiDashChart.tsx b/components/chartsDashboard/TingkatPrestasiDashChart.tsx new file mode 100644 index 0000000..56f7d9a --- /dev/null +++ b/components/chartsDashboard/TingkatPrestasiDashChart.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"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TingkatPrestasiData { + tahun_angkatan: string | number; + tingkat_prestasi: string; + tingkat_mahasiswa_prestasi: number; +} + +interface Props { + selectedYear: string; +} + +export default function TingkatPrestasiDashChart({ 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); + setError(null); + + const url = `/api/mahasiswa/tingkat-prestasi-dashboard?tahunAngkatan=${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'); + } + + // API already returns sorted data, no need to sort again + 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(); + }, [selectedYear]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) { + return []; + } + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => Number(a) - Number(b)); + const tingkatTypes = [...new Set(data.map(item => item.tingkat_prestasi))].sort(); + + return tingkatTypes.map(tingkat => ({ + name: tingkat, + data: years.map(year => { + const item = data.find(d => String(d.tahun_angkatan) === String(year) && d.tingkat_prestasi === tingkat); + return item?.tingkat_mahasiswa_prestasi || 0; + }) + })); + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val?.toString() || '0'; + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => Number(a) - Number(b)), + 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: 'Jumlah Mahasiswa', + 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' + }, + tickAmount: 5, + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + + ); + } + + return ( + + + + Tingkat Prestasi Dashboard + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && series.length > 0 ? ( + + ) : ( +
+

+ {series.length === 0 ? 'Tidak ada data untuk ditampilkan' : 'Loading chart...'} +

+
+ )} +
+
+
+ ); +} diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index d7a67d7..73e4d8e 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -115,7 +115,7 @@ const Navbar = () => { Dashboard - + {/*