revisi nih boz
This commit is contained in:
@@ -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<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 (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
@@ -17,7 +52,7 @@ export default function ClientLayout({ children }: ClientLayoutProps) {
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="min-h-screen">
|
||||
<Navbar />
|
||||
{showNavbar && <Navbar />}
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
@@ -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 `
|
||||
<div style="
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
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: {
|
||||
...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 `
|
||||
<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]);
|
||||
@@ -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)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
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 { 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 */}
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
{/* Beranda - Always visible */}
|
||||
<Link href="/" className="flex items-center gap-2 px-3 py-2 text-sm font-medium hover:text-primary transition-colors">
|
||||
<School className="h-4 w-4" />
|
||||
Beranda
|
||||
</Link>
|
||||
|
||||
{/* Dashboard Dropdown - Only when logged in */}
|
||||
{user && (
|
||||
{/* Dashboard - Only for Ketua Jurusan */}
|
||||
{user && user.role_user === 'ketuajurusan' && (
|
||||
<>
|
||||
<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" />
|
||||
@@ -205,10 +198,10 @@ const Navbar = () => {
|
||||
)}
|
||||
</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">
|
||||
|
||||
{user ? (
|
||||
{user && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="flex items-center gap-2">
|
||||
@@ -228,8 +221,6 @@ const Navbar = () => {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<LoginDialog onLoginSuccess={handleLoginSuccess} />
|
||||
)}
|
||||
<ThemeToggle />
|
||||
|
||||
@@ -264,19 +255,18 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => {
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
|
||||
{user ? (
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
<BarChart className="h-4 w-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* <div className="space-y-1">
|
||||
<h4 className="text-xs font-medium text-muted-foreground px-3">Visualisasi</h4>
|
||||
@@ -339,14 +329,7 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user