diff --git a/app/detail/jumlah-mahasiswa/page.tsx b/app/detail/jumlah-mahasiswa/page.tsx index 0ab8bbd..0fb8bed 100644 --- a/app/detail/jumlah-mahasiswa/page.tsx +++ b/app/detail/jumlah-mahasiswa/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart"; import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart"; import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import TabelJumlahMahasiswa from "@/components/chartstable/tabeljumlahmahasiswa"; export default function JumlahMahasiswaDetailPage() { const [selectedYear, setSelectedYear] = useState("all"); @@ -40,6 +41,11 @@ export default function JumlahMahasiswaDetailPage() { )} + {/* Tabel Section - Hanya tampil ketika "all" dipilih */} + {selectedYear === "all" && ( + + )} + {/* Information Section */}

diff --git a/app/page.tsx b/app/page.tsx index 6c7efb5..1bb0222 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,114 +1,326 @@ 'use client'; import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; import { useTheme } from "next-themes"; -import DashboardStats, { DashboardStatsSkeleton } from "@/components/dashboard/DashboardStats"; -import DashboardCharts from "@/components/dashboard/DashboardCharts"; +import { useToast } from "@/components/ui/toast-provider"; -interface MahasiswaTotal { - total_mahasiswa: number; - mahasiswa_aktif: number; - total_lulus: number; - pria_lulus: number; - wanita_lulus: number; - total_berprestasi: number; - prestasi_akademik: number; - prestasi_non_akademik: number; - total_mahasiswa_aktif_lulus: number; - total_mahasiswa_beasiswa: number; - total_mahasiswa_beasiswa_pemerintah: number; - total_mahasiswa_beasiswa_non_pemerintah: number; +interface UserData { + id_user: number; + username?: string; + nip?: string; + role_user: string; } - -export default function DashboardPage() { +export default function HomePage() { const { theme } = useTheme(); - const [mahasiswaData, setMahasiswaData] = useState({ - total_mahasiswa: 0, - mahasiswa_aktif: 0, - total_lulus: 0, - pria_lulus: 0, - wanita_lulus: 0, - total_berprestasi: 0, - prestasi_akademik: 0, - prestasi_non_akademik: 0, - total_mahasiswa_aktif_lulus: 0, - total_mahasiswa_beasiswa: 0, - total_mahasiswa_beasiswa_pemerintah: 0, - total_mahasiswa_beasiswa_non_pemerintah: 0 - }); - - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const router = useRouter(); + const { showSuccess, showError } = useToast(); + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [showLoginDialog, setShowLoginDialog] = useState(false); + // Check for existing user session on mount useEffect(() => { - const fetchData = async () => { - try { - // Menggunakan cache API untuk mempercepat loading - const cacheKey = 'mahasiswa-total-data'; - const cachedData = sessionStorage.getItem(cacheKey); - const cachedTimestamp = sessionStorage.getItem(`${cacheKey}-timestamp`); - - // Cek apakah data cache masih valid (kurang dari 60 detik) - const isCacheValid = cachedTimestamp && - (Date.now() - parseInt(cachedTimestamp)) < 60000; - - if (cachedData && isCacheValid) { - setMahasiswaData(JSON.parse(cachedData)); - } - - // Fetch data total mahasiswa - const totalResponse = await fetch('/api/mahasiswa/total', { - cache: 'no-store', - }); - - if (!totalResponse.ok) { - throw new Error('Failed to fetch total data'); - } - - const totalData = await totalResponse.json(); - setMahasiswaData(totalData); - - // Menyimpan data dan timestamp ke sessionStorage - sessionStorage.setItem(cacheKey, JSON.stringify(totalData)); - sessionStorage.setItem(`${cacheKey}-timestamp`, Date.now().toString()); - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - console.error('Error fetching data:', err); - } finally { - setLoading(false); - } - }; - - fetchData(); + checkUserSession(); }, []); - return ( -
-

Visualisasi Data Akademik Mahasiswa Informatika

- - {loading ? ( - - ) : error ? ( -
-
-
- - - -
-
-

{error}

-
-
-
- ) : ( - <> - - - - ) + const checkUserSession = async () => { + try { + const response = await fetch('/api/auth/user'); + if (response.ok) { + const data = await response.json(); + setUser(data.user); + // Redirect based on user role + if (data.user.role_user === 'ketuajurusan') { + router.push('/dashboard'); + } else if (data.user.role_user === 'admin') { + router.push('/keloladata/mahasiswa'); + } + } else { + // No user session, show login dialog + setShowLoginDialog(true); } + } catch (error) { + console.error('Error checking session:', error); + setShowLoginDialog(true); + } finally { + setIsLoading(false); + } + }; + + const handleLoginSuccess = (userData: any) => { + setUser(userData.user); + setShowLoginDialog(false); + + // Redirect based on user role + if (userData.user.role_user === 'ketuajurusan') { + showSuccess("Berhasil!", "Selamat datang, Ketua Jurusan!"); + router.push('/dashboard'); + } else if (userData.user.role_user === 'admin') { + showSuccess("Berhasil!", "Selamat datang, Admin!"); + router.push('/keloladata/mahasiswa'); + } + }; + + if (isLoading) { + return ( +
+
+
+

Memuat...

+
+
+ ); + } + + return ( +
+ {/* Background decorative elements */} +
+
+
+
+ +
+ {/* Header */} +
+

+ Portal Data Informatika +

+
+ + {showLoginDialog && ( + + )} +
+
+ ); +} + +// Auto-opening login dialog component +interface AutoLoginDialogProps { + onLoginSuccess: (userData: any) => void; +} + +function AutoLoginDialog({ onLoginSuccess }: AutoLoginDialogProps) { + const { showSuccess, showError } = useToast(); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState("ketua"); + + // Ketua Jurusan form state + const [ketuaForm, setKetuaForm] = useState({ + nip: "", + password: "", + }); + + // Admin form state + const [adminForm, setAdminForm] = useState({ + username: "", + password: "", + }); + + const handleKetuaLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + nip: ketuaForm.nip, + password: ketuaForm.password, + role: "ketuajurusan", + }), + }); + + const data = await response.json(); + + if (response.ok) { + onLoginSuccess(data); + setKetuaForm({ nip: "", password: "" }); + } else { + showError("Gagal!", data.message || "NIP atau password salah"); + } + } catch (error) { + showError("Gagal!", "Terjadi kesalahan saat login"); + } finally { + setIsLoading(false); + } + }; + + const handleAdminLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: adminForm.username, + password: adminForm.password, + role: "admin", + }), + }); + + const data = await response.json(); + + if (response.ok) { + onLoginSuccess(data); + setAdminForm({ username: "", password: "" }); + } else { + showError("Gagal!", data.message || "Username atau password salah"); + } + } catch (error) { + showError("Gagal!", "Terjadi kesalahan saat login"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Subtle glow effect */} +
+ +
+
+

+ Login ke PODIF +

+

+ Silakan login untuk melanjutkan +

+
+ +
+ {/* Tab buttons */} +
+ + +
+ + {/* Ketua Jurusan form */} + {activeTab === "ketua" && ( +
+
+ + setKetuaForm({ ...ketuaForm, nip: e.target.value })} + required + className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + /> +
+
+ + setKetuaForm({ ...ketuaForm, password: e.target.value })} + required + className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + /> +
+ +
+ )} + + {/* Admin form */} + {activeTab === "admin" && ( +
+
+ + setAdminForm({ ...adminForm, username: e.target.value })} + required + className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + /> +
+
+ + setAdminForm({ ...adminForm, password: e.target.value })} + required + className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + /> +
+ +
+ )} +
+
); } diff --git a/components/ClientLayout.tsx b/components/ClientLayout.tsx index 2d1c302..a78e2d5 100644 --- a/components/ClientLayout.tsx +++ b/components/ClientLayout.tsx @@ -1,5 +1,7 @@ "use client"; +import { useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; import { ThemeProvider } from '@/components/theme-provider'; import { Toaster } from '@/components/ui/toaster'; import Navbar from '@/components/ui/Navbar'; @@ -8,7 +10,40 @@ interface ClientLayoutProps { children: React.ReactNode; } +interface UserData { + id_user: number; + username?: string; + nip?: string; + role_user: string; +} + export default function ClientLayout({ children }: ClientLayoutProps) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const pathname = usePathname(); + + // Check for existing user session on mount + useEffect(() => { + checkUserSession(); + }, []); + + const checkUserSession = async () => { + try { + const response = await fetch('/api/auth/user'); + if (response.ok) { + const data = await response.json(); + setUser(data.user); + } + } catch (error) { + console.error('Error checking session:', error); + } finally { + setIsLoading(false); + } + }; + + // Don't show navbar on the root page (login page) + const showNavbar = pathname !== '/' && user; + return (
- + {showNavbar && }
{children}
diff --git a/components/charts/StatistikMahasiswaChart.tsx b/components/charts/StatistikMahasiswaChart.tsx index 9ed9e16..5334655 100644 --- a/components/charts/StatistikMahasiswaChart.tsx +++ b/components/charts/StatistikMahasiswaChart.tsx @@ -34,7 +34,7 @@ export default function StatistikMahasiswaChart({ const [chartOptions, setChartOptions] = useState({ chart: { type: 'bar' as const, - stacked: false, + stacked: true, toolbar: { show: true, tools: { @@ -69,8 +69,24 @@ export default function StatistikMahasiswaChart({ }, dataLabels: { enabled: true, - formatter: function (val: number) { - return val.toString() + formatter: function (val: number, opts: any) { + const seriesIndex = opts.seriesIndex; + const dataPointIndex = opts.dataPointIndex; + + // Jika series Total (index 2), tampilkan angka + if (seriesIndex === 2) { + return val.toString(); + } + + // Untuk Laki-laki (index 0) dan Perempuan (index 1), hitung persentase + // Ambil data total dari series Total (index 2) + const totalSeriesData = opts.w.config.series[2]?.data || []; + const totalValue = totalSeriesData[dataPointIndex] || 0; + + if (totalValue === 0 || val === 0) return '0%'; + + const percentage = ((val / totalValue) * 100).toFixed(1); + return percentage + '%'; }, position: 'top', style: { @@ -137,26 +153,59 @@ export default function StatistikMahasiswaChart({ colors: '#000' } }, - colors: ['#3B82F6', '#10B981', '#EC4899'], + colors: ['#3B82F6', '#EC4899', '#10B981'], tooltip: { theme: 'light', - y: [ - { - formatter: function (val: number) { - return val + " mahasiswa" - } - }, - { - formatter: function (val: number) { - return val + " mahasiswa" - } - }, - { - formatter: function (val: number) { - return val + " mahasiswa" - } - } - ] + shared: true, + intersect: false, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const lakiLaki = series[0][dataPointIndex]; + const perempuan = series[1][dataPointIndex]; + const total = series[2][dataPointIndex]; + const tahun = w.globals.labels[dataPointIndex]; + + return ` +
+
Angkatan ${tahun}
+
+
+ Laki-laki + ${lakiLaki} +
+
+
+ Perempuan + ${perempuan} +
+
+
+ Total + ${total} +
+
+ `; + } } }); @@ -198,6 +247,25 @@ export default function StatistikMahasiswaChart({ }, dataLabels: { ...prev.dataLabels, + formatter: function (val: number, opts: any) { + const seriesIndex = opts.seriesIndex; + const dataPointIndex = opts.dataPointIndex; + + // Jika series Total (index 2), tampilkan angka + if (seriesIndex === 2) { + return val.toString(); + } + + // Untuk Laki-laki (index 0) dan Perempuan (index 1), hitung persentase + // Ambil data total dari series Total (index 2) + const totalSeriesData = opts.w.config.series[2]?.data || []; + const totalValue = totalSeriesData[dataPointIndex] || 0; + + if (totalValue === 0 || val === 0) return '0%'; + + const percentage = ((val / totalValue) * 100).toFixed(0); + return percentage + '%'; + }, style: { ...prev.dataLabels.style, colors: [textColor] @@ -248,7 +316,61 @@ export default function StatistikMahasiswaChart({ }, tooltip: { ...prev.tooltip, - theme: tooltipTheme + theme: tooltipTheme, + custom: function({ series, seriesIndex, dataPointIndex, w }: any) { + const lakiLaki = series[0][dataPointIndex]; + const perempuan = series[1][dataPointIndex]; + const total = series[2][dataPointIndex]; + const tahun = w.globals.labels[dataPointIndex]; + + const bgColor = currentTheme === 'dark' ? '#1e293b' : 'white'; + const textColor = currentTheme === 'dark' ? '#fff' : '#000'; + const borderColor = currentTheme === 'dark' ? '#475569' : '#ccc'; + + const isDark = currentTheme === 'dark'; + + return ` +
+
Angkatan ${tahun}
+
+
+ Laki-laki + ${lakiLaki} +
+
+
+ Perempuan + ${perempuan} +
+
+
+ Total + ${total} +
+
+ `; + } } })); }, [theme]); @@ -278,15 +400,15 @@ export default function StatistikMahasiswaChart({ type: 'bar' as const, data: statistikData.map(item => item.pria) }, - { - name: 'Total', - type: 'bar' as const, - data: statistikData.map(item => item.total_mahasiswa) - }, { name: 'Perempuan', type: 'bar' as const, data: statistikData.map(item => item.wanita) + }, + { + name: 'Total', + type: 'bar' as const, + data: statistikData.map(item => item.total_mahasiswa) } ]; diff --git a/components/chartstable/tabeljumlahmahasiswa.tsx b/components/chartstable/tabeljumlahmahasiswa.tsx new file mode 100644 index 0000000..fe3e9a8 --- /dev/null +++ b/components/chartstable/tabeljumlahmahasiswa.tsx @@ -0,0 +1,155 @@ +'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 { Loader2 } from "lucide-react"; + +interface MahasiswaStatistik { + tahun_angkatan: number; + total_mahasiswa: number; + pria: number; + wanita: number; +} + +export default function TabelJumlahMahasiswa() { + const [statistikData, setStatistikData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const statistikResponse = await fetch('/api/mahasiswa/statistik', { + cache: 'no-store', + }); + + if (!statistikResponse.ok) { + throw new Error('Failed to fetch statistik data'); + } + + const statistikData = await statistikResponse.json(); + setStatistikData(statistikData); + } catch (err) { + setError(err instanceof Error ? err.message : 'Terjadi kesalahan'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + // Hitung total keseluruhan + const grandTotal = { + total_mahasiswa: statistikData.reduce((sum, item) => sum + item.total_mahasiswa, 0), + pria: statistikData.reduce((sum, item) => sum + item.pria, 0), + wanita: statistikData.reduce((sum, item) => sum + item.wanita, 0), + }; + + if (loading) { + return ( + + + +
+ + Loading... +
+
+
+
+ ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + +
+ + + + Tahun Angkatan + Laki-laki + Perempuan + Total + + + + {statistikData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + <> + {statistikData.map((item, index) => ( + + + {item.tahun_angkatan} + + +
{item.pria}
+
+ +
{item.wanita}
+
+ +
+ {item.total_mahasiswa} +
+
+
+ ))} + + {/* Total Row */} + + + TOTAL + + +
{grandTotal.pria}
+
+ +
{grandTotal.wanita}
+
+ +
+ {grandTotal.total_mahasiswa} +
+
+
+ + )} +
+
+
+ +
+
+ ); +} diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index 8bc2126..b54614f 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { ThemeToggle } from '@/components/theme-toggle'; -import { Menu, ChevronDown, BarChart, Database, CircleCheck, School, GraduationCap, Clock, BookOpen, Award, Home, LogOut, User, Users } from 'lucide-react'; +import { Menu, ChevronDown, BarChart, Database, GraduationCap, BookOpen, Award, LogOut, User, Users } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { @@ -14,7 +14,6 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import Link from 'next/link'; -import LoginDialog from './login-dialog'; import { useToast } from '@/components/ui/toast-provider'; interface UserData { @@ -102,14 +101,8 @@ const Navbar = () => { {/* Desktop Navigation - Centered */}
- {/* Beranda - Always visible */} - - - Beranda - - - {/* Dashboard Dropdown - Only when logged in */} - {user && ( + {/* Dashboard - Only for Ketua Jurusan */} + {user && user.role_user === 'ketuajurusan' && ( <> @@ -205,10 +198,10 @@ const Navbar = () => { )}
- {/* Right Side - Theme Toggle, Login/User Menu, and Mobile Menu */} + {/* Right Side - Theme Toggle, User Menu, and Mobile Menu */}
- {user ? ( + {user && (
- ) : ( -
-

Login

-

- Silakan login untuk mengakses menu Visualisasi dan Kelola Data -

-
- )} + ) : null}
); };