559 lines
20 KiB
TypeScript
559 lines
20 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import Link from 'next/link'
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
LineChart,
|
|
Line,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
} from 'recharts'
|
|
|
|
interface User {
|
|
id: string
|
|
email: string
|
|
name: string
|
|
role: string
|
|
profileImage?: string
|
|
studentId?: string
|
|
teacherId?: string
|
|
}
|
|
|
|
interface DashboardStats {
|
|
totalStudents: number
|
|
totalTeachers: number
|
|
totalClasses: number
|
|
totalSubjects: number
|
|
}
|
|
|
|
interface ChartData {
|
|
enrollmentData: { name: string; students: number }[]
|
|
attendanceData: {
|
|
month: string
|
|
present: number
|
|
absent: number
|
|
late: number
|
|
}[]
|
|
gradeData: { grade: string; count: number }[]
|
|
subjectData: { name: string; teachers: number }[]
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const [user, setUser] = useState<User | null>(null)
|
|
const [stats, setStats] = useState<DashboardStats | null>(null)
|
|
const [chartData, setChartData] = useState<ChartData | null>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const router = useRouter()
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('token')
|
|
if (!token) {
|
|
router.push('/auth/login')
|
|
return
|
|
}
|
|
|
|
// Parse JWT to get user info (in production, verify with server)
|
|
try {
|
|
const payload = JSON.parse(atob(token.split('.')[1]))
|
|
setUser(payload)
|
|
fetchDashboardData()
|
|
} catch (error) {
|
|
console.error('Invalid token:', error)
|
|
localStorage.removeItem('token')
|
|
router.push('/auth/login')
|
|
}
|
|
}, [router])
|
|
|
|
const fetchDashboardData = async () => {
|
|
try {
|
|
const token = localStorage.getItem('token')
|
|
|
|
// Fetch stats
|
|
const statsResponse = await fetch('/api/dashboard/stats', {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
|
|
if (statsResponse.ok) {
|
|
const statsData = await statsResponse.json()
|
|
setStats(statsData)
|
|
}
|
|
|
|
// Fetch chart data
|
|
const chartsResponse = await fetch('/api/dashboard/charts', {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
|
|
if (chartsResponse.ok) {
|
|
const chartsData = await chartsResponse.json()
|
|
setChartData(chartsData)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch dashboard data:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('token')
|
|
router.push('/auth/login')
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className='min-h-screen flex items-center justify-center'>
|
|
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600'></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Statistik kehadiran Jarwo untuk dashboard admin
|
|
const jarwoAttendanceWeek = [
|
|
{ day: 'Senin', hadir: true },
|
|
{ day: 'Selasa', hadir: false },
|
|
{ day: 'Rabu', hadir: true },
|
|
{ day: 'Kamis', hadir: false },
|
|
{ day: 'Jumat', hadir: true },
|
|
]
|
|
const jarwoHadirCount = jarwoAttendanceWeek.filter((a) => a.hadir).length
|
|
|
|
if (user?.role === 'ADMIN' && stats) {
|
|
return (
|
|
<div className='min-h-screen bg-gray-50'>
|
|
<nav className='bg-white shadow-sm border-b'>
|
|
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
|
|
<div className='flex justify-between h-16'>
|
|
<div className='flex items-center'>
|
|
<h1 className='text-xl font-bold text-gray-900'>SIPINTAR</h1>
|
|
<span className='ml-2 text-sm text-gray-500'>
|
|
Dashboard Admin
|
|
</span>
|
|
</div>
|
|
<div className='flex items-center space-x-4'>
|
|
<span className='text-sm text-gray-700'>
|
|
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
|
|
</span>
|
|
<button
|
|
onClick={handleLogout}
|
|
className='text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium'
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8'>
|
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'>
|
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
|
<div className='flex items-center'>
|
|
<div className='p-3 rounded-full bg-blue-100'>
|
|
<svg
|
|
className='w-6 h-6 text-blue-600'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z'
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className='ml-4'>
|
|
<p className='text-sm font-medium text-gray-500'>
|
|
Total Siswa
|
|
</p>
|
|
<p className='text-2xl font-semibold text-gray-900'>
|
|
{stats.totalStudents}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
|
<div className='flex items-center'>
|
|
<div className='p-3 rounded-full bg-green-100'>
|
|
<svg
|
|
className='w-6 h-6 text-green-600'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z'
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className='ml-4'>
|
|
<p className='text-sm font-medium text-gray-500'>
|
|
Total Guru
|
|
</p>
|
|
<p className='text-2xl font-semibold text-gray-900'>
|
|
{stats.totalTeachers}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
|
<div className='flex items-center'>
|
|
<div className='p-3 rounded-full bg-purple-100'>
|
|
<svg
|
|
className='w-6 h-6 text-purple-600'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className='ml-4'>
|
|
<p className='text-sm font-medium text-gray-500'>
|
|
Total Kelas
|
|
</p>
|
|
<p className='text-2xl font-semibold text-gray-900'>
|
|
{stats.totalClasses}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
|
<div className='flex items-center'>
|
|
<div className='p-3 rounded-full bg-yellow-100'>
|
|
<svg
|
|
className='w-6 h-6 text-yellow-600'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253'
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className='ml-4'>
|
|
<p className='text-sm font-medium text-gray-500'>
|
|
Mata Pelajaran
|
|
</p>
|
|
<p className='text-2xl font-semibold text-gray-900'>
|
|
{stats.totalSubjects}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Statistik Kehadiran Jarwo */}
|
|
<div className='mb-8'>
|
|
<h3 className='text-2xl font-bold text-blue-700 mb-6 flex items-center gap-2'>
|
|
<svg
|
|
className='w-7 h-7 text-blue-500'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z'
|
|
/>
|
|
</svg>
|
|
Statistik Kehadiran Siswa:{' '}
|
|
<span className='text-blue-900'>Jarwo</span>
|
|
</h3>
|
|
<div className='bg-gradient-to-br from-blue-50 to-green-50 rounded-xl shadow-lg border border-blue-100 p-8 mb-6'>
|
|
<div className='flex items-center justify-between mb-4'>
|
|
<div>
|
|
<p className='text-lg font-semibold text-gray-900'>
|
|
Kehadiran Minggu Ini
|
|
</p>
|
|
<p className='text-sm text-gray-500'>
|
|
Periode: <span className='font-medium'>Senin - Jumat</span>
|
|
</p>
|
|
</div>
|
|
<div className='flex items-center gap-2'>
|
|
<span className='px-3 py-1 rounded-full bg-green-100 text-green-700 font-bold text-sm flex items-center gap-1'>
|
|
<svg
|
|
className='w-4 h-4'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M5 13l4 4L19 7'
|
|
/>
|
|
</svg>
|
|
{jarwoHadirCount} Hadir
|
|
</span>
|
|
<span className='px-3 py-1 rounded-full bg-red-100 text-red-700 font-bold text-sm flex items-center gap-1'>
|
|
<svg
|
|
className='w-4 h-4'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M6 18L18 6M6 6l12 12'
|
|
/>
|
|
</svg>
|
|
{5 - jarwoHadirCount} Tidak Hadir
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 mb-4'>
|
|
{jarwoAttendanceWeek.map((a, idx) => (
|
|
<div
|
|
key={idx}
|
|
className={`flex flex-col items-center py-3 rounded-lg border ${
|
|
a.hadir
|
|
? 'bg-green-50 border-green-200'
|
|
: 'bg-red-50 border-red-200'
|
|
}`}
|
|
>
|
|
<span className='text-gray-700 font-medium mb-1'>
|
|
{a.day}
|
|
</span>
|
|
<span
|
|
className={
|
|
a.hadir
|
|
? 'text-green-600 font-bold'
|
|
: 'text-red-500 font-bold'
|
|
}
|
|
>
|
|
{a.hadir ? (
|
|
<span className='flex items-center gap-1'>
|
|
<svg
|
|
className='w-4 h-4'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M5 13l4 4L19 7'
|
|
/>
|
|
</svg>
|
|
Hadir
|
|
</span>
|
|
) : (
|
|
<span className='flex items-center gap-1'>
|
|
<svg
|
|
className='w-4 h-4'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M6 18L18 6M6 6l12 12'
|
|
/>
|
|
</svg>
|
|
Tidak Hadir
|
|
</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className='mt-4'>
|
|
<p className='text-sm text-gray-600 mb-2'>
|
|
Progress Kehadiran Mingguan
|
|
</p>
|
|
<div className='w-full h-5 bg-gray-200 rounded-full overflow-hidden'>
|
|
<div
|
|
className='h-5 bg-gradient-to-r from-green-400 to-blue-400 rounded-full transition-all duration-500'
|
|
style={{ width: `${(jarwoHadirCount / 7) * 100}%` }}
|
|
></div>
|
|
</div>
|
|
<p className='text-xs text-gray-500 mt-2'>
|
|
{jarwoHadirCount} dari 7 hari hadir
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className='bg-white rounded-xl shadow-lg border border-blue-100 p-8'>
|
|
<h4 className='text-lg font-semibold text-blue-700 mb-4 flex items-center gap-2'>
|
|
<svg
|
|
className='w-5 h-5 text-blue-400'
|
|
fill='none'
|
|
stroke='currentColor'
|
|
viewBox='0 0 24 24'
|
|
>
|
|
<path
|
|
strokeLinecap='round'
|
|
strokeLinejoin='round'
|
|
strokeWidth={2}
|
|
d='M9 17v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
|
|
/>
|
|
</svg>
|
|
Grafik Kehadiran Mingguan
|
|
</h4>
|
|
<ResponsiveContainer width='100%' height={180}>
|
|
<BarChart
|
|
data={jarwoAttendanceWeek.map((a) => ({
|
|
day: a.day,
|
|
hadir: a.hadir ? 1 : 0,
|
|
}))}
|
|
>
|
|
<CartesianGrid strokeDasharray='3 3' />
|
|
<XAxis dataKey='day' />
|
|
<YAxis allowDecimals={false} domain={[0, 1]} />
|
|
<Tooltip
|
|
formatter={(value) =>
|
|
value === 1 ? 'Hadir' : 'Tidak Hadir'
|
|
}
|
|
/>
|
|
<Bar dataKey='hadir' fill='#38bdf8' radius={[8, 8, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Custom dashboard for student named Jarwo
|
|
if (user?.role === 'STUDENT' && user?.name?.toLowerCase() === 'jarwo') {
|
|
// Simulated attendance data for Jarwo
|
|
const attendanceWeek = [
|
|
{ day: 'Senin', hadir: true },
|
|
{ day: 'Selasa', hadir: false },
|
|
{ day: 'Rabu', hadir: true },
|
|
{ day: 'Kamis', hadir: false },
|
|
{ day: 'Jumat', hadir: true },
|
|
]
|
|
const hadirCount = attendanceWeek.filter((a) => a.hadir).length
|
|
return (
|
|
<div className='min-h-screen bg-gray-50'>
|
|
<nav className='bg-white shadow-sm border-b'>
|
|
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
|
|
<div className='flex justify-between h-16'>
|
|
<div className='flex items-center'>
|
|
<h1 className='text-xl font-bold text-gray-900'>SIPINTAR</h1>
|
|
<span className='ml-2 text-sm text-gray-500'>
|
|
Dashboard Siswa
|
|
</span>
|
|
</div>
|
|
<div className='flex items-center space-x-4'>
|
|
<span className='text-sm text-gray-700'>
|
|
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
|
|
</span>
|
|
<button
|
|
onClick={handleLogout}
|
|
className='text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium'
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div className='max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8'>
|
|
<div className='mb-8'>
|
|
<h2 className='text-2xl font-bold text-gray-900'>
|
|
Statistik Kehadiran Mingguan
|
|
</h2>
|
|
<p className='text-gray-600 mb-2'>
|
|
Siswa: <strong>Jarwo</strong>
|
|
</p>
|
|
<div className='bg-white rounded-lg shadow-sm border p-6 mb-6'>
|
|
<p className='text-lg font-semibold text-gray-900 mb-2'>
|
|
Kehadiran Minggu Ini
|
|
</p>
|
|
<div className='flex flex-col gap-2'>
|
|
{attendanceWeek.map((a, idx) => (
|
|
<div key={idx} className='flex justify-between items-center'>
|
|
<span className='text-gray-700'>{a.day}</span>
|
|
<span
|
|
className={
|
|
a.hadir
|
|
? 'text-green-600 font-bold'
|
|
: 'text-red-500 font-bold'
|
|
}
|
|
>
|
|
{a.hadir ? 'Hadir' : 'Tidak Hadir'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className='mt-4'>
|
|
<p className='text-sm text-gray-600'>
|
|
Total hadir:{' '}
|
|
<span className='font-bold text-green-600'>{hadirCount}</span>{' '}
|
|
dari 7 hari
|
|
</p>
|
|
<div className='w-full h-4 bg-gray-200 rounded-full mt-2'>
|
|
<div
|
|
className='h-4 bg-green-500 rounded-full'
|
|
style={{ width: `${(hadirCount / 7) * 100}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className='bg-white rounded-lg shadow-sm border p-6'>
|
|
<h3 className='text-lg font-semibold text-gray-900 mb-2'>
|
|
Grafik Kehadiran
|
|
</h3>
|
|
<ResponsiveContainer width='100%' height={220}>
|
|
<BarChart
|
|
data={attendanceWeek.map((a) => ({
|
|
day: a.day,
|
|
hadir: a.hadir ? 1 : 0,
|
|
}))}
|
|
>
|
|
<CartesianGrid strokeDasharray='3 3' />
|
|
<XAxis dataKey='day' />
|
|
<YAxis allowDecimals={false} domain={[0, 1]} />
|
|
<Tooltip
|
|
formatter={(value) =>
|
|
value === 1 ? 'Hadir' : 'Tidak Hadir'
|
|
}
|
|
/>
|
|
<Bar dataKey='hadir' fill='#10B981' />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
// ...existing code...
|
|
}
|