revisi nih boz
This commit is contained in:
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart";
|
import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart";
|
||||||
import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart";
|
import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart";
|
||||||
import FilterTahunAngkatan from "@/components/FilterTahunAngkatan";
|
import FilterTahunAngkatan from "@/components/FilterTahunAngkatan";
|
||||||
|
import TabelJumlahMahasiswa from "@/components/chartstable/tabeljumlahmahasiswa";
|
||||||
|
|
||||||
export default function JumlahMahasiswaDetailPage() {
|
export default function JumlahMahasiswaDetailPage() {
|
||||||
const [selectedYear, setSelectedYear] = useState<string>("all");
|
const [selectedYear, setSelectedYear] = useState<string>("all");
|
||||||
@@ -40,6 +41,11 @@ export default function JumlahMahasiswaDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tabel Section - Hanya tampil ketika "all" dipilih */}
|
||||||
|
{selectedYear === "all" && (
|
||||||
|
<TabelJumlahMahasiswa />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Information Section */}
|
{/* Information Section */}
|
||||||
<div className="bg-white dark:bg-slate-900 rounded-lg shadow-sm p-6">
|
<div className="bg-white dark:bg-slate-900 rounded-lg shadow-sm p-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
|||||||
408
app/page.tsx
408
app/page.tsx
@@ -1,114 +1,326 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import DashboardStats, { DashboardStatsSkeleton } from "@/components/dashboard/DashboardStats";
|
import { useToast } from "@/components/ui/toast-provider";
|
||||||
import DashboardCharts from "@/components/dashboard/DashboardCharts";
|
|
||||||
|
|
||||||
interface MahasiswaTotal {
|
interface UserData {
|
||||||
total_mahasiswa: number;
|
id_user: number;
|
||||||
mahasiswa_aktif: number;
|
username?: string;
|
||||||
total_lulus: number;
|
nip?: string;
|
||||||
pria_lulus: number;
|
role_user: string;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
export default function DashboardPage() {
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [mahasiswaData, setMahasiswaData] = useState<MahasiswaTotal>({
|
const router = useRouter();
|
||||||
total_mahasiswa: 0,
|
const { showSuccess, showError } = useToast();
|
||||||
mahasiswa_aktif: 0,
|
const [user, setUser] = useState<UserData | null>(null);
|
||||||
total_lulus: 0,
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
pria_lulus: 0,
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||||
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<string | null>(null);
|
|
||||||
|
|
||||||
|
// Check for existing user session on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
checkUserSession();
|
||||||
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();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
const checkUserSession = async () => {
|
||||||
<div className="container mx-auto p-4 space-y-6">
|
try {
|
||||||
<h1 className="text-2xl font-bold mb-4">Visualisasi Data Akademik Mahasiswa Informatika</h1>
|
const response = await fetch('/api/auth/user');
|
||||||
|
if (response.ok) {
|
||||||
{loading ? (
|
const data = await response.json();
|
||||||
<DashboardStatsSkeleton />
|
setUser(data.user);
|
||||||
) : error ? (
|
// Redirect based on user role
|
||||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-4">
|
if (data.user.role_user === 'ketuajurusan') {
|
||||||
<div className="flex">
|
router.push('/dashboard');
|
||||||
<div className="flex-shrink-0">
|
} else if (data.user.role_user === 'admin') {
|
||||||
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
router.push('/keloladata/mahasiswa');
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
}
|
||||||
</svg>
|
} else {
|
||||||
</div>
|
// No user session, show login dialog
|
||||||
<div className="ml-3">
|
setShowLoginDialog(true);
|
||||||
<p className="text-sm text-red-700">{error}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<DashboardStats mahasiswaData={mahasiswaData} />
|
|
||||||
<DashboardCharts />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
} 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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||||||
|
<p className="text-muted-foreground">Memuat...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-blue-900 to-slate-800 relative overflow-hidden">
|
||||||
|
{/* Background decorative elements */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-sm mx-auto p-4 relative z-10">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center space-y-3 mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-white">
|
||||||
|
Portal Data Informatika
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showLoginDialog && (
|
||||||
|
<AutoLoginDialog onLoginSuccess={handleLoginSuccess} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="bg-slate-800/95 backdrop-blur-md rounded-xl shadow-xl p-6 w-full border border-slate-600/50 relative">
|
||||||
|
{/* Subtle glow effect */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/5 to-purple-500/5 rounded-xl"></div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-1">
|
||||||
|
Login ke PODIF
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-300 text-sm">
|
||||||
|
Silakan login untuk melanjutkan
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Tab buttons */}
|
||||||
|
<div className="flex rounded-lg bg-slate-700/50 p-1 border border-slate-600">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("ketua")}
|
||||||
|
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||||
|
activeTab === "ketua"
|
||||||
|
? "bg-blue-600 text-white shadow-md"
|
||||||
|
: "text-slate-300 hover:text-white hover:bg-slate-600/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Ketua Jurusan
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("admin")}
|
||||||
|
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||||
|
activeTab === "admin"
|
||||||
|
? "bg-blue-600 text-white shadow-md"
|
||||||
|
: "text-slate-300 hover:text-white hover:bg-slate-600/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ketua Jurusan form */}
|
||||||
|
{activeTab === "ketua" && (
|
||||||
|
<form onSubmit={handleKetuaLogin} className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label htmlFor="nip" className="block text-sm font-medium text-slate-200">
|
||||||
|
NIP
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nip"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan NIP"
|
||||||
|
value={ketuaForm.nip}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label htmlFor="ketua-password" className="block text-sm font-medium text-slate-200">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="ketua-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Masukkan password"
|
||||||
|
value={ketuaForm.password}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:from-blue-400 disabled:to-blue-500 text-white font-medium py-2.5 px-4 rounded-lg transition-all duration-200 shadow-md"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Login sebagai Ketua Jurusan"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Admin form */}
|
||||||
|
{activeTab === "admin" && (
|
||||||
|
<form onSubmit={handleAdminLogin} className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label htmlFor="username" className="block text-sm font-medium text-slate-200">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan username"
|
||||||
|
value={adminForm.username}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label htmlFor="admin-password" className="block text-sm font-medium text-slate-200">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="admin-password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Masukkan password"
|
||||||
|
value={adminForm.password}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:from-blue-400 disabled:to-blue-500 text-white font-medium py-2.5 px-4 rounded-lg transition-all duration-200 shadow-md"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Login sebagai Admin"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
import { ThemeProvider } from '@/components/theme-provider';
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
import Navbar from '@/components/ui/Navbar';
|
import Navbar from '@/components/ui/Navbar';
|
||||||
@@ -8,7 +10,40 @@ interface ClientLayoutProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id_user: number;
|
||||||
|
username?: string;
|
||||||
|
nip?: string;
|
||||||
|
role_user: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ClientLayout({ children }: ClientLayoutProps) {
|
export default function ClientLayout({ children }: ClientLayoutProps) {
|
||||||
|
const [user, setUser] = useState<UserData | null>(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 (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
@@ -17,7 +52,7 @@ export default function ClientLayout({ children }: ClientLayoutProps) {
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navbar />
|
{showNavbar && <Navbar />}
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function StatistikMahasiswaChart({
|
|||||||
const [chartOptions, setChartOptions] = useState({
|
const [chartOptions, setChartOptions] = useState({
|
||||||
chart: {
|
chart: {
|
||||||
type: 'bar' as const,
|
type: 'bar' as const,
|
||||||
stacked: false,
|
stacked: true,
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: true,
|
show: true,
|
||||||
tools: {
|
tools: {
|
||||||
@@ -69,8 +69,24 @@ export default function StatistikMahasiswaChart({
|
|||||||
},
|
},
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
formatter: function (val: number) {
|
formatter: function (val: number, opts: any) {
|
||||||
return val.toString()
|
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',
|
position: 'top',
|
||||||
style: {
|
style: {
|
||||||
@@ -137,26 +153,59 @@ export default function StatistikMahasiswaChart({
|
|||||||
colors: '#000'
|
colors: '#000'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: ['#3B82F6', '#10B981', '#EC4899'],
|
colors: ['#3B82F6', '#EC4899', '#10B981'],
|
||||||
tooltip: {
|
tooltip: {
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
y: [
|
shared: true,
|
||||||
{
|
intersect: false,
|
||||||
formatter: function (val: number) {
|
custom: function({ series, seriesIndex, dataPointIndex, w }: any) {
|
||||||
return val + " mahasiswa"
|
const lakiLaki = series[0][dataPointIndex];
|
||||||
}
|
const perempuan = series[1][dataPointIndex];
|
||||||
},
|
const total = series[2][dataPointIndex];
|
||||||
{
|
const tahun = w.globals.labels[dataPointIndex];
|
||||||
formatter: function (val: number) {
|
|
||||||
return val + " mahasiswa"
|
return `
|
||||||
}
|
<div style="
|
||||||
},
|
padding: 12px 16px;
|
||||||
{
|
background: rgba(255, 255, 255, 0.95);
|
||||||
formatter: function (val: number) {
|
backdrop-filter: blur(10px);
|
||||||
return val + " mahasiswa"
|
border: none;
|
||||||
}
|
border-radius: 12px;
|
||||||
}
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
]
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-width: 180px;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
">Angkatan ${tahun}</div>
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 4px;">
|
||||||
|
<div style="width: 8px; height: 8px; background: #3B82F6; border-radius: 50%; margin-right: 8px;"></div>
|
||||||
|
<span style="font-size: 12px; color: #374151;">Laki-laki</span>
|
||||||
|
<span style="font-size: 12px; font-weight: 600; color: #1f2937; margin-left: auto;">${lakiLaki}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 4px;">
|
||||||
|
<div style="width: 8px; height: 8px; background: #EC4899; border-radius: 50%; margin-right: 8px;"></div>
|
||||||
|
<span style="font-size: 12px; color: #374151;">Perempuan</span>
|
||||||
|
<span style="font-size: 12px; font-weight: 600; color: #1f2937; margin-left: auto;">${perempuan}</span>
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
">
|
||||||
|
<div style="width: 8px; height: 8px; background: #10B981; border-radius: 50%; margin-right: 8px;"></div>
|
||||||
|
<span style="font-size: 12px; font-weight: 600; color: #1f2937;">Total</span>
|
||||||
|
<span style="font-size: 13px; font-weight: 700; color: #10B981; margin-left: auto;">${total}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -198,6 +247,25 @@ export default function StatistikMahasiswaChart({
|
|||||||
},
|
},
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
...prev.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: {
|
style: {
|
||||||
...prev.dataLabels.style,
|
...prev.dataLabels.style,
|
||||||
colors: [textColor]
|
colors: [textColor]
|
||||||
@@ -248,7 +316,61 @@ export default function StatistikMahasiswaChart({
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
...prev.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 `
|
||||||
|
<div style="
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: ${isDark ? 'rgba(30, 41, 59, 0.95)' : 'rgba(255, 255, 255, 0.95)'};
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px ${isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)'};
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
min-width: 180px;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${isDark ? '#f1f5f9' : '#1f2937'};
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
">Angkatan ${tahun}</div>
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 4px;">
|
||||||
|
<div style="width: 8px; height: 8px; background: #3B82F6; border-radius: 50%; margin-right: 8px;"></div>
|
||||||
|
<span style="font-size: 12px; color: ${isDark ? '#cbd5e1' : '#374151'};">Laki-laki</span>
|
||||||
|
<span style="font-size: 12px; font-weight: 600; color: ${isDark ? '#f1f5f9' : '#1f2937'}; margin-left: auto;">${lakiLaki}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 4px;">
|
||||||
|
<div style="width: 8px; height: 8px; background: #EC4899; border-radius: 50%; margin-right: 8px;"></div>
|
||||||
|
<span style="font-size: 12px; color: ${isDark ? '#cbd5e1' : '#374151'};">Perempuan</span>
|
||||||
|
<span style="font-size: 12px; font-weight: 600; color: ${isDark ? '#f1f5f9' : '#1f2937'}; margin-left: auto;">${perempuan}</span>
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
border-top: 1px solid ${isDark ? '#475569' : '#e5e7eb'};
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
">
|
||||||
|
<div style="width: 8px; height: 8px; background: #10B981; border-radius: 50%; margin-right: 8px;"></div>
|
||||||
|
<span style="font-size: 12px; font-weight: 600; color: ${isDark ? '#f1f5f9' : '#1f2937'};">Total</span>
|
||||||
|
<span style="font-size: 13px; font-weight: 700; color: #10B981; margin-left: auto;">${total}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
@@ -278,15 +400,15 @@ export default function StatistikMahasiswaChart({
|
|||||||
type: 'bar' as const,
|
type: 'bar' as const,
|
||||||
data: statistikData.map(item => item.pria)
|
data: statistikData.map(item => item.pria)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Total',
|
|
||||||
type: 'bar' as const,
|
|
||||||
data: statistikData.map(item => item.total_mahasiswa)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Perempuan',
|
name: 'Perempuan',
|
||||||
type: 'bar' as const,
|
type: 'bar' as const,
|
||||||
data: statistikData.map(item => item.wanita)
|
data: statistikData.map(item => item.wanita)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Total',
|
||||||
|
type: 'bar' as const,
|
||||||
|
data: statistikData.map(item => item.total_mahasiswa)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
155
components/chartstable/tabeljumlahmahasiswa.tsx
Normal file
155
components/chartstable/tabeljumlahmahasiswa.tsx
Normal file
@@ -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<MahasiswaStatistik[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl font-bold dark:text-white">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl font-bold text-red-500 dark:text-red-400">
|
||||||
|
Error: {error}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardContent>
|
||||||
|
<div className="border rounded-md overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-gray-50 dark:bg-slate-800">
|
||||||
|
<TableHead className="font-semibold text-center">Tahun Angkatan</TableHead>
|
||||||
|
<TableHead className="font-semibold text-center">Laki-laki</TableHead>
|
||||||
|
<TableHead className="font-semibold text-center">Perempuan</TableHead>
|
||||||
|
<TableHead className="font-semibold text-center">Total</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{statistikData.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">
|
||||||
|
Tidak ada data yang tersedia
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{statistikData.map((item, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={item.tahun_angkatan}
|
||||||
|
className={index % 2 === 0 ? "bg-white dark:bg-slate-900" : "bg-gray-50/50 dark:bg-slate-800/50"}
|
||||||
|
>
|
||||||
|
<TableCell className="text-center font-medium dark:text-white">
|
||||||
|
{item.tahun_angkatan}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="font-medium dark:text-white">{item.pria}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="font-medium dark:text-white">{item.wanita}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="font-semibold dark:text-white">
|
||||||
|
{item.total_mahasiswa}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Total Row */}
|
||||||
|
<TableRow className="bg-blue-50 dark:bg-blue-900/20 border-t-2 border-blue-200 dark:border-blue-700 dark:text-white">
|
||||||
|
<TableCell className="text-center font-bold">
|
||||||
|
TOTAL
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="font-bold dark:text-white">{grandTotal.pria}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="font-bold dark:text-white">{grandTotal.wanita}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="font-bold text-lg">
|
||||||
|
{grandTotal.total_mahasiswa}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ThemeToggle } from '@/components/theme-toggle';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import LoginDialog from './login-dialog';
|
|
||||||
import { useToast } from '@/components/ui/toast-provider';
|
import { useToast } from '@/components/ui/toast-provider';
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
@@ -102,14 +101,8 @@ const Navbar = () => {
|
|||||||
|
|
||||||
{/* Desktop Navigation - Centered */}
|
{/* Desktop Navigation - Centered */}
|
||||||
<div className="hidden md:flex items-center gap-4">
|
<div className="hidden md:flex items-center gap-4">
|
||||||
{/* Beranda - Always visible */}
|
{/* Dashboard - Only for Ketua Jurusan */}
|
||||||
<Link href="/" className="flex items-center gap-2 px-3 py-2 text-sm font-medium hover:text-primary transition-colors">
|
{user && user.role_user === 'ketuajurusan' && (
|
||||||
<School className="h-4 w-4" />
|
|
||||||
Beranda
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Dashboard Dropdown - Only when logged in */}
|
|
||||||
{user && (
|
|
||||||
<>
|
<>
|
||||||
<Link href="/dashboard" className="flex items-center gap-2 px-3 py-2 text-sm font-medium hover:text-primary transition-colors">
|
<Link href="/dashboard" className="flex items-center gap-2 px-3 py-2 text-sm font-medium hover:text-primary transition-colors">
|
||||||
<BarChart className="h-4 w-4" />
|
<BarChart className="h-4 w-4" />
|
||||||
@@ -205,10 +198,10 @@ const Navbar = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side - Theme Toggle, Login/User Menu, and Mobile Menu */}
|
{/* Right Side - Theme Toggle, User Menu, and Mobile Menu */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|
||||||
{user ? (
|
{user && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="flex items-center gap-2">
|
<Button variant="ghost" className="flex items-center gap-2">
|
||||||
@@ -228,8 +221,6 @@ const Navbar = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
|
||||||
<LoginDialog onLoginSuccess={handleLoginSuccess} />
|
|
||||||
)}
|
)}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|
||||||
@@ -264,19 +255,18 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => {
|
|||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground">Dashboard PODIF</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground">Dashboard PODIF</h3>
|
||||||
<Link href="/" className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md transition-colors">
|
|
||||||
<Home className="h-4 w-4" />
|
|
||||||
Beranda
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground">Menu Utama</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground">Menu Utama</h3>
|
||||||
|
{/* Dashboard - Only for Ketua Jurusan */}
|
||||||
|
{user.role_user === 'ketuajurusan' && (
|
||||||
<Link href="/dashboard" className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md transition-colors">
|
<Link href="/dashboard" className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md transition-colors">
|
||||||
<BarChart className="h-4 w-4" />
|
<BarChart className="h-4 w-4" />
|
||||||
Dashboard
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* <div className="space-y-1">
|
{/* <div className="space-y-1">
|
||||||
<h4 className="text-xs font-medium text-muted-foreground px-3">Visualisasi</h4>
|
<h4 className="text-xs font-medium text-muted-foreground px-3">Visualisasi</h4>
|
||||||
@@ -339,14 +329,7 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : null}
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground">Login</h3>
|
|
||||||
<p className="text-sm text-muted-foreground px-3">
|
|
||||||
Silakan login untuk mengakses menu Visualisasi dan Kelola Data
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user