From f7a735940831c7a13117c901d9295025f25ab641 Mon Sep 17 00:00:00 2001 From: Randa Firman Putra Date: Thu, 21 Aug 2025 00:13:35 +0700 Subject: [PATCH] Change update semester --- app/api/keloladata/update-semester/route.ts | 25 +- .../kelompok-keahlian-status/route.ts | 96 ++++++++ app/visualisasi/dashboard/page.tsx | 147 ++++++++++++ .../page.tsx | 0 app/visualisasi/status/page.tsx | 12 +- components/charts/IPKLulusTepatChart.tsx | 6 +- .../charts/JenisPendaftaranLulusPieChart.tsx | 3 - .../charts/KelompokKeahlianStatusChart.tsx | 224 ++++++++++++++++++ .../charts/KelompokKeahlianStatusPieChart.tsx | 158 ++++++++++++ components/charts/LulusTepatWaktuPieChart.tsx | 1 - components/charts/StatistikMahasiswaChart.tsx | 9 - components/datatable/data-table-mahasiswa.tsx | 10 +- components/ui/Navbar.tsx | 14 +- 13 files changed, 667 insertions(+), 38 deletions(-) create mode 100644 app/api/mahasiswa/kelompok-keahlian-status/route.ts create mode 100644 app/visualisasi/dashboard/page.tsx rename app/visualisasi/{tipekelulusan => lulustepatwaktu}/page.tsx (100%) create mode 100644 components/charts/KelompokKeahlianStatusChart.tsx create mode 100644 components/charts/KelompokKeahlianStatusPieChart.tsx diff --git a/app/api/keloladata/update-semester/route.ts b/app/api/keloladata/update-semester/route.ts index 37dc807..3c10b94 100644 --- a/app/api/keloladata/update-semester/route.ts +++ b/app/api/keloladata/update-semester/route.ts @@ -42,22 +42,23 @@ export async function POST() { } // Calculate current semester based on tahun_angkatan and current date - const yearsSinceEnrollment = currentYear - tahunAngkatan; - let currentSemester = yearsSinceEnrollment * 2; // 2 semesters per year + const years = currentYear - tahunAngkatan; + let currentSemester: number; - // Adjust for current month (odd months = odd semesters, even months = even semesters) - if (currentMonth >= 2 && currentMonth <= 7) { - // February to July = odd semester (1, 3, 5, etc.) - currentSemester += 1; + if (currentMonth >= 8 && currentMonth <= 12) { + // Agustus–Desember: semester ganjil tahun akademik berjalan + currentSemester = years * 2 + 1; + } else if (currentMonth >= 2 && currentMonth <= 7) { + // Februari–Juli: semester genap tahun akademik berjalan + currentSemester = years * 2 + 2; } else { - // August to January = even semester (2, 4, 6, etc.) - currentSemester += 2; + // Januari: masih semester ganjil dari T.A. sebelumnya + currentSemester = (years - 1) * 2 + 1; } - // Cap at semester 14 (7 years) - if (currentSemester > 14) { - currentSemester = 14; - } + // Jaga batas + if (currentSemester < 1) currentSemester = 1; + if (currentSemester > 14) currentSemester = 14; // Update semester if different if (student.semester !== currentSemester) { diff --git a/app/api/mahasiswa/kelompok-keahlian-status/route.ts b/app/api/mahasiswa/kelompok-keahlian-status/route.ts new file mode 100644 index 0000000..e37f988 --- /dev/null +++ b/app/api/mahasiswa/kelompok-keahlian-status/route.ts @@ -0,0 +1,96 @@ +import { NextResponse } from 'next/server'; +import supabase from '@/lib/db'; + +interface KelompokKeahlianStatus { + tahun_angkatan: number; + status_kuliah: string; + nama_kelompok: string; + jumlah_mahasiswa: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const statusKuliah = searchParams.get('status_kuliah'); + + try { + let query = supabase + .from('mahasiswa') + .select('tahun_angkatan, status_kuliah, id_kelompok_keahlian, kelompok_keahlian!inner(id_kk, nama_kelompok)') + .eq('status_kuliah', statusKuliah); + + 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, status_kuliah, nama_kelompok + const groupedData = data.reduce((acc, item: any) => { + const tahun_angkatan = item.tahun_angkatan; + const status_kuliah = item.status_kuliah; + const nama_kelompok = item.kelompok_keahlian?.nama_kelompok; + const key = `${tahun_angkatan}-${status_kuliah}-${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, status_kuliah, nama_kelompok] = key.split('-'); + return { + tahun_angkatan: parseInt(tahun_angkatan), + status_kuliah, + 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/visualisasi/dashboard/page.tsx b/app/visualisasi/dashboard/page.tsx new file mode 100644 index 0000000..8b4e6df --- /dev/null +++ b/app/visualisasi/dashboard/page.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { useState } from "react"; +import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart"; +import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart"; +import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart"; +import AsalDaerahChart from "@/components/charts/AsalDaerahChart"; +import IPKChart from "@/components/charts/IPKChart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import JenisPendaftaranPerAngkatanChart from "@/components/charts/JenisPendaftaranPerAngkatanChart"; +import AsalDaerahPerAngkatanChart from "@/components/charts/AsalDaerahPerAngkatanChart"; +import StatusMahasiswaChart from "@/components/charts/StatusMahasiswaChart"; + +export default function TotalMahasiswaPage() { + const [selectedYear, setSelectedYear] = useState("all"); + + return ( +
+ {/* Header */} +
+
+
+

+ Dashboard Mahasiswa +

+

+ Data visualisasi mahasiswa per jenis pendaftaran, status, IPK, dan asal daerah +

+
+
+ +
+
+
+ + {selectedYear === "all" ? ( + /* Layout untuk semua data */ +
+ {/* Row 1: Main chart + Side chart */} +
+ {/* Statistik Mahasiswa - Large */} +
+
+

+ Statistik Mahasiswa (Keseluruhan) +

+
+ +
+
+
+ + {/* Jenis Pendaftaran */} +
+
+

+ Jenis Pendaftaran Mahasiswa +

+
+ +
+
+
+
+ + {/* Row 2: Three equal charts */} +
+ {/* Status Mahasiswa */} +
+

+ Status Mahasiswa +

+
+ +
+
+ + {/* IPK Chart */} +
+

+ Rata-rata IPK per Program Studi +

+
+ +
+
+ + {/* Asal Daerah */} +
+

+ Distribusi Asal Daerah Mahasiswa +

+
+ +
+
+
+
+ ) : ( + /* Layout untuk data per angkatan */ +
+ {/* Row 1: Main chart + Side chart */} +
+ {/* Statistik Per Angkatan - Large */} +
+
+

+ Statistik Mahasiswa Angkatan {selectedYear} +

+
+ +
+
+
+ + {/* Jenis Pendaftaran Per Angkatan */} +
+
+

+ Jenis Pendaftaran Angkatan {selectedYear} +

+
+ +
+
+
+
+ + {/* Row 2: Full width chart */} +
+
+

+ Asal Daerah Mahasiswa Angkatan {selectedYear} +

+
+ +
+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/visualisasi/tipekelulusan/page.tsx b/app/visualisasi/lulustepatwaktu/page.tsx similarity index 100% rename from app/visualisasi/tipekelulusan/page.tsx rename to app/visualisasi/lulustepatwaktu/page.tsx diff --git a/app/visualisasi/status/page.tsx b/app/visualisasi/status/page.tsx index f7fae34..d3dea7b 100644 --- a/app/visualisasi/status/page.tsx +++ b/app/visualisasi/status/page.tsx @@ -11,6 +11,8 @@ 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"; +import KelompokKeahlianStatusChart from "@/components/charts/KelompokKeahlianStatusChart"; +import KelompokKeahlianStatusPieChart from "@/components/charts/KelompokKeahlianStatusPieChart"; export default function StatusMahasiswaPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -44,7 +46,7 @@ export default function StatusMahasiswaPage() { {selectedYear === "all" ? ( <>
- @@ -56,6 +58,10 @@ export default function StatusMahasiswaPage() { selectedYear={selectedYear} selectedStatus={selectedStatus} /> + + -
+
diff --git a/components/charts/JenisPendaftaranLulusPieChart.tsx b/components/charts/JenisPendaftaranLulusPieChart.tsx index 05b6e0b..adaedf0 100644 --- a/components/charts/JenisPendaftaranLulusPieChart.tsx +++ b/components/charts/JenisPendaftaranLulusPieChart.tsx @@ -169,9 +169,6 @@ export default function JenisPendaftaranLulusPieChart({ selectedYear }: Props) { Jenis Pendaftaran Mahasiswa Lulus Tepat Waktu {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} -
- Total Mahasiswa: {totalMahasiswa} -
diff --git a/components/charts/KelompokKeahlianStatusChart.tsx b/components/charts/KelompokKeahlianStatusChart.tsx new file mode 100644 index 0000000..06abfea --- /dev/null +++ b/components/charts/KelompokKeahlianStatusChart.tsx @@ -0,0 +1,224 @@ +'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; + status_kuliah: string; + nama_kelompok: string; + jumlah_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function KelompokKeahlianStatusChart({ selectedYear, selectedStatus }: 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/kelompok-keahlian-status?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}` + ); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = await response.json(); + // Sort data by nama_kelompok + 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, selectedStatus]); + + // 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 {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/charts/KelompokKeahlianStatusPieChart.tsx b/components/charts/KelompokKeahlianStatusPieChart.tsx new file mode 100644 index 0000000..903d034 --- /dev/null +++ b/components/charts/KelompokKeahlianStatusPieChart.tsx @@ -0,0 +1,158 @@ +'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; + status_kuliah: string; + nama_kelompok: string; + jumlah_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function KelompokKeahlianStatusPieChart({ selectedYear, selectedStatus }: 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/kelompok-keahlian-status?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}` + ); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = await response.json(); + // Sort data by nama_kelompok + 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, selectedStatus]); + + // Pie chart: label = nama_kelompok, value = jumlah_mahasiswa + const labels = data.map(item => item.nama_kelompok); + const series = data.map(item => item.jumlah_mahasiswa); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels, + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + }, + 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' + }, + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + ' mahasiswa'; + } + } + }, + colors: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0', '#8B5CF6', '#EC4899', '#06B6D4', '#F97316'], + }; + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Kelompok Keahlian Mahasiswa {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/charts/LulusTepatWaktuPieChart.tsx b/components/charts/LulusTepatWaktuPieChart.tsx index d0d6971..cd6dc94 100644 --- a/components/charts/LulusTepatWaktuPieChart.tsx +++ b/components/charts/LulusTepatWaktuPieChart.tsx @@ -109,7 +109,6 @@ export default function LulusTepatWaktuPieChart({ selectedYear }: Props) { } }; - // Process data for series const processSeriesData = () => { const maleData = data.find(item => item.jk === 'Pria')?.jumlah_lulus_tepat_waktu || 0; const femaleData = data.find(item => item.jk === 'Wanita')?.jumlah_lulus_tepat_waktu || 0; diff --git a/components/charts/StatistikMahasiswaChart.tsx b/components/charts/StatistikMahasiswaChart.tsx index 1f79df9..747b522 100644 --- a/components/charts/StatistikMahasiswaChart.tsx +++ b/components/charts/StatistikMahasiswaChart.tsx @@ -304,13 +304,6 @@ export default function StatistikMahasiswaChart() { } return ( - - - - Total Mahasiswa - - -
-
-
); } \ No newline at end of file diff --git a/components/datatable/data-table-mahasiswa.tsx b/components/datatable/data-table-mahasiswa.tsx index 2b463e4..2c2ce05 100644 --- a/components/datatable/data-table-mahasiswa.tsx +++ b/components/datatable/data-table-mahasiswa.tsx @@ -489,6 +489,10 @@ export default function DataTableMahasiswa() {

Data Mahasiswa

+
@@ -865,7 +865,7 @@ export default function DataTableMahasiswa() { Aktif Cuti Lulus - DO + Non-Aktif
diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index f87c453..6453edf 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -119,6 +119,12 @@ const Navbar = () => { + + + + Dashboard Mahasiswa + + @@ -132,9 +138,9 @@ const Navbar = () => { - + - Tipe Kelulusan + Lulus Tepat Waktu @@ -266,9 +272,9 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => { Status Kuliah - + - Tipe Kelulusan + Lulus Tepat Waktu