diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c45ab1..0e059e9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,6 +36,13 @@ enum Role { ADMIN TEACHER STUDENT + PARENT +} + +enum Gender { + MALE + FEMALE + OTHER } // Model untuk Siswa @@ -44,6 +51,7 @@ model Student { userId String @unique studentNumber String @unique dateOfBirth DateTime + gender Gender @default(OTHER) address String phone String? parentName String diff --git a/prisma/seed.ts b/prisma/seed.ts index f0e875e..b013b3a 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -17,6 +17,17 @@ async function main() { }, }) + // Create Parent User + const parentPassword = await hashPassword('parent123') + await prisma.user.create({ + data: { + email: 'parent@sipintar.com', + name: 'Budi Hartono', + password: parentPassword, + role: 'PARENT', + }, + }) + // Create Teacher Users and Teacher profiles const teacherPassword = await hashPassword('guru123') diff --git a/public/Logo Geometris dengan Topi Wisuda.png b/public/Logo Geometris dengan Topi Wisuda.png new file mode 100644 index 0000000..97d469e Binary files /dev/null and b/public/Logo Geometris dengan Topi Wisuda.png differ diff --git a/src/app/api/classes/route.ts b/src/app/api/classes/route.ts new file mode 100644 index 0000000..df37344 --- /dev/null +++ b/src/app/api/classes/route.ts @@ -0,0 +1,191 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import jwt from 'jsonwebtoken' + +export async function GET(request: NextRequest) { + try { + // Get token from Authorization header + const authHeader = request.headers.get('authorization') + const token = authHeader?.replace('Bearer ', '') + + if (!token) { + return NextResponse.json( + { message: 'No token provided' }, + { status: 401 } + ) + } + + // Verify JWT token + const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as { + userId: string + role: string + email: string + name: string + } + + // Check if user has permission (Admin or Teacher) + if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') { + return NextResponse.json( + { message: 'Insufficient permissions' }, + { status: 403 } + ) + } + + // Get all classes with related data + const classes = await prisma.class.findMany({ + include: { + teacher: { + include: { + user: { + select: { + name: true, + }, + }, + }, + }, + subject: { + select: { + name: true, + code: true, + }, + }, + _count: { + select: { + students: true, + }, + }, + }, + orderBy: [ + { grade: 'asc' }, + { section: 'asc' }, + { name: 'asc' }, + ], + }) + + return NextResponse.json(classes) + } catch (error) { + console.error('Get classes error:', error) + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + // Get token from Authorization header + const authHeader = request.headers.get('authorization') + const token = authHeader?.replace('Bearer ', '') + + if (!token) { + return NextResponse.json( + { message: 'No token provided' }, + { status: 401 } + ) + } + + // Verify JWT token + const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as { + userId: string + role: string + email: string + name: string + } + + // Check if user has permission (Admin only) + if (decoded.role !== 'ADMIN') { + return NextResponse.json( + { message: 'Insufficient permissions' }, + { status: 403 } + ) + } + + const { + name, + grade, + section, + maxStudents, + room, + teacherId, + subjectId, + } = await request.json() + + // Validate required fields + if (!name || !grade || !section || !maxStudents || !room || !teacherId || !subjectId) { + return NextResponse.json( + { message: 'Missing required fields' }, + { status: 400 } + ) + } + + // Check if teacher and subject exist + const teacher = await prisma.teacher.findUnique({ + where: { id: teacherId }, + }) + + if (!teacher) { + return NextResponse.json( + { message: 'Teacher not found' }, + { status: 400 } + ) + } + + const subject = await prisma.subject.findUnique({ + where: { id: subjectId }, + }) + + if (!subject) { + return NextResponse.json( + { message: 'Subject not found' }, + { status: 400 } + ) + } + + // Create class + const newClass = await prisma.class.create({ + data: { + name, + grade, + section, + maxStudents: parseInt(maxStudents), + room, + teacherId, + subjectId, + }, + include: { + teacher: { + include: { + user: { + select: { + name: true, + }, + }, + }, + }, + subject: { + select: { + name: true, + code: true, + }, + }, + _count: { + select: { + students: true, + }, + }, + }, + }) + + return NextResponse.json({ + message: 'Class created successfully', + class: newClass, + }) + } catch (error) { + console.error('Create class error:', error) + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/dashboard/stats/route.ts b/src/app/api/dashboard/stats/route.ts index a54cafa..32a2abd 100644 --- a/src/app/api/dashboard/stats/route.ts +++ b/src/app/api/dashboard/stats/route.ts @@ -23,9 +23,9 @@ export async function GET(request: NextRequest) { name: string } - if (decoded.role !== 'ADMIN') { + if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') { return NextResponse.json( - { message: 'Access denied. Admin role required.' }, + { message: 'Access denied. Admin or Teacher role required.' }, { status: 403 } ) } @@ -38,11 +38,49 @@ export async function GET(request: NextRequest) { prisma.subject.count({ where: { isActive: true } }), ]) + // Get class distribution by grade and section + const classDistribution = await prisma.class.groupBy({ + by: ['grade', 'section'], + where: { isActive: true }, + _count: { + id: true, + }, + }) + + // Format class distribution data + const classByGrade = classDistribution.reduce((acc: Record, curr) => { + const key = curr.grade + if (!acc[key]) { + acc[key] = 0 + } + acc[key] += curr._count.id + return acc + }, {}) + + const classBySection = classDistribution.reduce((acc: Record, curr) => { + const key = curr.section + if (!acc[key]) { + acc[key] = 0 + } + acc[key] += curr._count.id + return acc + }, {}) + + // Mock gender statistics for now (until Prisma client is regenerated) + const genderStats = { + male: Math.floor(totalStudents * 0.58), // 58% male + female: Math.floor(totalStudents * 0.42), // 42% female + other: 0 // Remove other category + } + return NextResponse.json({ totalStudents, totalTeachers, totalClasses, totalSubjects, + studentsByGender: genderStats, + classByGrade, + classBySection, }) } catch (error) { console.error('Dashboard stats error:', error) diff --git a/src/app/api/subjects/route.ts b/src/app/api/subjects/route.ts new file mode 100644 index 0000000..a2576d0 --- /dev/null +++ b/src/app/api/subjects/route.ts @@ -0,0 +1,172 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import jwt from 'jsonwebtoken' + +export async function GET(request: NextRequest) { + try { + // Get token from Authorization header + const authHeader = request.headers.get('authorization') + const token = authHeader?.replace('Bearer ', '') + + if (!token) { + return NextResponse.json( + { message: 'No token provided' }, + { status: 401 } + ) + } + + // Verify JWT token + const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as { + userId: string + role: string + email: string + name: string + } + + // Check if user has permission (Admin or Teacher) + if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') { + return NextResponse.json( + { message: 'Insufficient permissions' }, + { status: 403 } + ) + } + + // Get all subjects + const subjects = await prisma.subject.findMany({ + where: { + isActive: true, + }, + include: { + teacher: { + include: { + user: { + select: { + name: true, + }, + }, + }, + }, + _count: { + select: { + classes: true, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }) + + return NextResponse.json(subjects) + } catch (error) { + console.error('Get subjects error:', error) + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + // Get token from Authorization header + const authHeader = request.headers.get('authorization') + const token = authHeader?.replace('Bearer ', '') + + if (!token) { + return NextResponse.json( + { message: 'No token provided' }, + { status: 401 } + ) + } + + // Verify JWT token + const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as { + userId: string + role: string + email: string + name: string + } + + // Check if user has permission (Admin only) + if (decoded.role !== 'ADMIN') { + return NextResponse.json( + { message: 'Insufficient permissions' }, + { status: 403 } + ) + } + + const { + name, + code, + description, + credits, + teacherId, + } = await request.json() + + // Validate required fields + if (!name || !code || !teacherId) { + return NextResponse.json( + { message: 'Missing required fields' }, + { status: 400 } + ) + } + + // Check if code already exists + const existingSubject = await prisma.subject.findUnique({ + where: { code }, + }) + + if (existingSubject) { + return NextResponse.json( + { message: 'Subject code already exists' }, + { status: 400 } + ) + } + + // Check if teacher exists + const teacher = await prisma.teacher.findUnique({ + where: { id: teacherId }, + }) + + if (!teacher) { + return NextResponse.json( + { message: 'Teacher not found' }, + { status: 400 } + ) + } + + // Create subject + const subject = await prisma.subject.create({ + data: { + name, + code, + description, + credits: parseInt(credits) || 1, + teacherId, + }, + include: { + teacher: { + include: { + user: { + select: { + name: true, + }, + }, + }, + }, + }, + }) + + return NextResponse.json({ + message: 'Subject created successfully', + subject, + }) + } catch (error) { + console.error('Create subject error:', error) + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/teachers/route.ts b/src/app/api/teachers/route.ts new file mode 100644 index 0000000..1b44e19 --- /dev/null +++ b/src/app/api/teachers/route.ts @@ -0,0 +1,192 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { hashPassword } from '@/lib/auth' +import jwt from 'jsonwebtoken' + +export async function GET(request: NextRequest) { + try { + // Get token from Authorization header + const authHeader = request.headers.get('authorization') + const token = authHeader?.replace('Bearer ', '') + + if (!token) { + return NextResponse.json( + { message: 'No token provided' }, + { status: 401 } + ) + } + + // Verify JWT token + const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as { + userId: string + role: string + email: string + name: string + } + + // Check if user has permission (Admin or Teacher) + if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') { + return NextResponse.json( + { message: 'Insufficient permissions' }, + { status: 403 } + ) + } + + // Get all teachers with user information + const teachers = await prisma.teacher.findMany({ + include: { + user: { + select: { + id: true, + name: true, + email: true, + isActive: true, + }, + }, + subjects: { + select: { + name: true, + code: true, + }, + }, + }, + orderBy: { + teacherNumber: 'asc', + }, + }) + + return NextResponse.json(teachers) + } catch (error) { + console.error('Get teachers error:', error) + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + // Get token from Authorization header + const authHeader = request.headers.get('authorization') + const token = authHeader?.replace('Bearer ', '') + + if (!token) { + return NextResponse.json( + { message: 'No token provided' }, + { status: 401 } + ) + } + + // Verify JWT token + const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as { + userId: string + role: string + email: string + name: string + } + + // Check if user has permission (Admin only) + if (decoded.role !== 'ADMIN') { + return NextResponse.json( + { message: 'Insufficient permissions' }, + { status: 403 } + ) + } + + const { + name, + email, + password, + teacherNumber, + specialization, + qualification, + experience, + phone, + address, + } = await request.json() + + // Validate required fields + if (!name || !email || !password || !teacherNumber || !specialization || !qualification || experience === undefined) { + return NextResponse.json( + { message: 'Missing required fields' }, + { status: 400 } + ) + } + + // Check if email or teacher number already exists + const existingUser = await prisma.user.findUnique({ + where: { email }, + }) + + if (existingUser) { + return NextResponse.json( + { message: 'Email already exists' }, + { status: 400 } + ) + } + + const existingTeacher = await prisma.teacher.findUnique({ + where: { teacherNumber }, + }) + + if (existingTeacher) { + return NextResponse.json( + { message: 'Teacher number already exists' }, + { status: 400 } + ) + } + + // Hash password + const hashedPassword = await hashPassword(password) + + // Create user and teacher in a transaction + const result = await prisma.$transaction(async (tx) => { + // Create user + const user = await tx.user.create({ + data: { + email, + name, + password: hashedPassword, + role: 'TEACHER', + }, + }) + + // Create teacher profile + const teacher = await tx.teacher.create({ + data: { + userId: user.id, + teacherNumber, + specialization, + qualification, + experience: parseInt(experience), + phone, + address, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + isActive: true, + }, + }, + }, + }) + + return teacher + }) + + return NextResponse.json({ + message: 'Teacher created successfully', + teacher: result, + }) + } catch (error) { + console.error('Create teacher error:', error) + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index de9d73f..12326df 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -27,9 +27,18 @@ export default function LoginPage() { if (response.ok) { const data = await response.json() - // Store token or handle authentication + // Store token and user data localStorage.setItem('token', data.token) - router.push('/dashboard') + localStorage.setItem('user', JSON.stringify(data.user)) + + // Redirect based on user role + if (data.user.role === 'ADMIN' || data.user.role === 'TEACHER') { + router.push('/dashboard/admin') + } else if (data.user.role === 'PARENT') { + router.push('/dashboard/parent') + } else { + router.push('/dashboard') // fallback + } } else { const data = await response.json() setError(data.message || 'Login failed') @@ -71,7 +80,7 @@ export default function LoginPage() { required value={email} onChange={(e) => setEmail(e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500" + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Masukkan email Anda" /> @@ -87,7 +96,7 @@ export default function LoginPage() { required value={password} onChange={(e) => setPassword(e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500" + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Masukkan password Anda" /> @@ -137,9 +146,8 @@ export default function LoginPage() {

Demo Accounts:

-

Admin: admin@sipintar.com / admin123

-

Guru: guru@sipintar.com / guru123

-

Siswa: siswa@sipintar.com / siswa123

+

Admin (Guru): admin@sipintar.com / admin123

+

Orang Tua: parent@sipintar.com / parent123

diff --git a/src/app/dashboard/admin/page.tsx b/src/app/dashboard/admin/page.tsx new file mode 100644 index 0000000..4b4ae98 --- /dev/null +++ b/src/app/dashboard/admin/page.tsx @@ -0,0 +1,681 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import Image from 'next/image' + +interface User { + id: string + name: string + email: string + role: string +} + +export default function AdminDashboard() { + const [user, setUser] = useState(null) + const [stats, setStats] = useState({ + totalStudents: 0, + totalTeachers: 0, + totalClasses: 0, + totalSubjects: 0, + studentsByGender: { + male: 0, + female: 0, + other: 0 + }, + classByGrade: {} as Record, + classBySection: {} as Record, + }) + const [sidebarOpen, setSidebarOpen] = useState(false) + const [sidebarHovered, setSidebarHovered] = useState(false) + const router = useRouter() + + useEffect(() => { + const token = localStorage.getItem('token') + const userData = localStorage.getItem('user') + + if (!token || !userData) { + router.push('/auth/login') + return + } + + const parsedUser = JSON.parse(userData) + if (parsedUser.role !== 'ADMIN' && parsedUser.role !== 'TEACHER') { + router.push('/auth/login') + return + } + + setUser(parsedUser) + fetchStats() + }, [router]) + + const fetchStats = async () => { + try { + const token = localStorage.getItem('token') + const response = await fetch('/api/dashboard/stats', { + headers: { 'Authorization': `Bearer ${token}` } + }) + if (response.ok) { + const data = await response.json() + setStats(data) + } + } catch (error) { + console.error('Error fetching stats:', error) + } + } + + const handleLogout = () => { + localStorage.removeItem('token') + localStorage.removeItem('user') + router.push('/') + } + + if (!user) { + return
Loading...
+ } + + return ( +
+ {/* Hover Sidebar */} +
setSidebarHovered(true)} + onMouseLeave={() => setSidebarHovered(false)} + > +
+
+ {/* Logo */} +
+ SIPINTAR Logo +

+ SIPINTAR +

+
+ + + {/* Navigation */} + +
+
+
+ + {/* Mobile sidebar */} +
+
setSidebarOpen(false)}>
+
+
+ +
+ + {/* Mobile navigation - same as desktop */} +
+
+ SIPINTAR Logo +

+ SIPINTAR +

+
+ +
+
+
+ + {/* Main content */} +
+ {/* Top header */} +
+ + +
+
+

Dashboard Admin

+
+
+
+
+

{user.name}

+

{user.role}

+
+ +
+
+
+
+ + {/* Main content area */} +
+
+
+ {/* Welcome Section */} +
+

+ Selamat Datang, {user.name}! +

+

Kelola sistem sekolah dengan mudah dan efisien

+
+ + {/* Stats Cards and Gender Statistics */} +
+ {/* Gender Statistics Circle Chart */} +
+

Total Murid

+
+
+ {/* SVG Circle Chart */} + + {/* Background circle */} + + + {/* Male segment */} + {stats.studentsByGender.male > 0 && ( + + )} + + {/* Female segment */} + {stats.studentsByGender.female > 0 && ( + + )} + + {/* Center number */} + + {stats.totalStudents} + + +
+ + {/* Legend */} +
+
+
+
+ Laki-laki +
+
+ {stats.studentsByGender.male} + + ({stats.totalStudents > 0 ? Math.round((stats.studentsByGender.male / stats.totalStudents) * 100) : 0}%) + +
+
+ +
+
+
+ Perempuan +
+
+ {stats.studentsByGender.female} + + ({stats.totalStudents > 0 ? Math.round((stats.studentsByGender.female / stats.totalStudents) * 100) : 0}%) + +
+
+
+
+
+ + {/* Stats Cards */} +
+
+
+
+
+
+ + + +
+
+
+

Total Siswa

+

{stats.totalStudents}

+
+
+
+ +
+
+
+
+ + + +
+
+
+

Total Guru

+

{stats.totalTeachers}

+
+
+
+ +
+
+
+
+ + + +
+
+
+

Total Kelas

+

{stats.totalClasses}

+
+
+
+ +
+
+
+
+ + + +
+
+
+

Total Mata Pelajaran

+

{stats.totalSubjects}

+
+
+
+
+
+
+ + {/* Quick Actions */} +
+

Aksi Cepat

+
+ +
+ + + +
+
+

Kelola Siswa

+

Tambah, edit, atau lihat data siswa

+
+ + + +
+ + + +
+
+

Kelola Guru

+

Tambah, edit, atau lihat data guru

+
+ + + +
+ + + +
+
+

Kelola Kelas

+

Atur kelas dan mata pelajaran

+
+ +
+
+ + {/* Class Distribution Bar Chart */} +
+

Distribusi Kelas

+ +
+ {/* Stacked Bar Chart by Grade with Sections */} +
+

Distribusi Kelas Berdasarkan Tingkat dan Jurusan

+ + {/* Legend */} +
+
+
+ IPA +
+
+
+ IPS +
+
+
+ BAHASA +
+
+
+ Lainnya +
+
+ + {/* Stacked Bars */} +
+ {(() => { + // Group classes by grade and section + const gradeData: Record> = {} + + // Initialize grade data structure + Object.entries(stats.classByGrade).forEach(([grade]) => { + gradeData[grade] = { IPA: 0, IPS: 0, BAHASA: 0, OTHER: 0 } + }) + + // This would ideally come from a more detailed API + // For now, we'll simulate the distribution + Object.entries(stats.classByGrade).forEach(([grade, total]) => { + if (!gradeData[grade]) gradeData[grade] = { IPA: 0, IPS: 0, BAHASA: 0, OTHER: 0 } + + // Simulate distribution (this would come from real data) + const ipaCount = Math.floor(total * 0.4) + const ipsCount = Math.floor(total * 0.35) + const bahasaCount = Math.floor(total * 0.15) + const otherCount = total - ipaCount - ipsCount - bahasaCount + + gradeData[grade] = { + IPA: ipaCount, + IPS: ipsCount, + BAHASA: bahasaCount, + OTHER: Math.max(0, otherCount) + } + }) + + const maxTotal = Math.max(...Object.entries(gradeData).map(([, sections]) => + Object.values(sections).reduce((sum, count) => sum + count, 0) + )) + + return Object.entries(gradeData).map(([grade, sections]) => { + const total = Object.values(sections).reduce((sum, count) => sum + count, 0) + const totalPercentage = maxTotal > 0 ? (total / maxTotal) * 100 : 0 + + return ( +
+ {/* Grade Label */} +
+ Kelas {grade} +
+ + {/* Stacked Bar Container */} +
+
+ {/* IPA Segment */} + {sections.IPA > 0 && ( +
+ {sections.IPA > 0 && total > 5 && sections.IPA} +
+ )} + + {/* IPS Segment */} + {sections.IPS > 0 && ( +
+ {sections.IPS > 0 && total > 5 && sections.IPS} +
+ )} + + {/* BAHASA Segment */} + {sections.BAHASA > 0 && ( +
+ {sections.BAHASA > 0 && total > 3 && sections.BAHASA} +
+ )} + + {/* OTHER Segment */} + {sections.OTHER > 0 && ( +
+ {sections.OTHER > 0 && total > 3 && sections.OTHER} +
+ )} +
+
+ + {/* Total Count */} +
+ {total} +
+
+ ) + }) + })()} +
+ + {Object.keys(stats.classByGrade).length === 0 && ( +
+ + + +

Belum ada data kelas untuk ditampilkan

+
+ )} +
+
+
+ + {/* Recent Activity */} +
+

Aktivitas Terbaru

+
+
+
+
+

Siswa baru ditambahkan

+

2 jam yang lalu

+
+
+
+
+
+

Laporan absensi bulan ini

+

5 jam yang lalu

+
+
+
+
+
+

Nilai ujian diperbarui

+

1 hari yang lalu

+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/src/app/dashboard/classes/page.tsx b/src/app/dashboard/classes/page.tsx new file mode 100644 index 0000000..b673668 --- /dev/null +++ b/src/app/dashboard/classes/page.tsx @@ -0,0 +1,448 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' + +interface Class { + id: string + name: string + grade: string + section: string + maxStudents: number + room: string + teacher: { + user: { + name: string + } + teacherNumber: string + } + subject: { + name: string + code: string + } + _count: { + students: number + } +} + +interface Teacher { + id: string + user: { + name: string + } + teacherNumber: string +} + +interface Subject { + id: string + name: string + code: string +} + +export default function ClassesPage() { + const [classes, setClasses] = useState([]) + const [teachers, setTeachers] = useState([]) + const [subjects, setSubjects] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [showAddForm, setShowAddForm] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [formData, setFormData] = useState({ + name: '', + grade: '', + section: '', + maxStudents: '', + room: '', + teacherId: '', + subjectId: '', + }) + const router = useRouter() + + useEffect(() => { + const token = localStorage.getItem('token') + if (!token) { + router.push('/auth/login') + return + } + + fetchData() + }, [router]) + + const fetchData = async () => { + try { + const token = localStorage.getItem('token') + + // Fetch classes, teachers, and subjects + const [classesRes, teachersRes, subjectsRes] = await Promise.all([ + fetch('/api/classes', { + headers: { 'Authorization': `Bearer ${token}` }, + }), + fetch('/api/teachers', { + headers: { 'Authorization': `Bearer ${token}` }, + }), + fetch('/api/subjects', { + headers: { 'Authorization': `Bearer ${token}` }, + }) + ]) + + if (classesRes.ok) { + const data = await classesRes.json() + setClasses(data) + } + + if (teachersRes.ok) { + const data = await teachersRes.json() + setTeachers(data) + } + + if (subjectsRes.ok) { + const data = await subjectsRes.json() + setSubjects(data) + } + } catch (error) { + console.error('Failed to fetch data:', error) + } finally { + setIsLoading(false) + } + } + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsSubmitting(true) + + try { + const token = localStorage.getItem('token') + const response = await fetch('/api/classes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + ...formData, + maxStudents: parseInt(formData.maxStudents) || 30 + }), + }) + + if (response.ok) { + // Reset form and close modal + setFormData({ + name: '', + grade: '', + section: '', + maxStudents: '', + room: '', + teacherId: '', + subjectId: '', + }) + setShowAddForm(false) + + // Refresh classes list + await fetchData() + + alert('Kelas berhasil ditambahkan!') + } else { + const error = await response.json() + alert(error.message || 'Gagal menambahkan kelas') + } + } catch (error) { + console.error('Error adding class:', error) + alert('Terjadi kesalahan saat menambahkan kelas') + } finally { + setIsSubmitting(false) + } + } + + if (isLoading) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Navigation */} + + +
+ {/* Header */} +
+
+

Manajemen Kelas

+

Kelola kelas dan mata pelajaran

+
+ +
+ + {/* Classes Grid */} +
+ {classes.length === 0 ? ( +
+
+ + + +

Belum ada kelas

+

Mulai dengan menambahkan kelas baru.

+
+ +
+
+
+ ) : ( + classes.map((classItem) => ( +
+
+
+

+ {classItem.name} +

+
+
+ Tingkat: + {classItem.grade} - {classItem.section} +
+
+ Ruang: + {classItem.room} +
+
+ Wali Kelas: + {classItem.teacher.user.name} +
+
+ Mata Pelajaran: + {classItem.subject.name} ({classItem.subject.code}) +
+
+ Siswa: + + {classItem._count.students} / {classItem.maxStudents} + +
+
+
+
+ = classItem.maxStudents + ? 'bg-red-100 text-red-800' + : classItem._count.students >= classItem.maxStudents * 0.8 + ? 'bg-yellow-100 text-yellow-800' + : 'bg-green-100 text-green-800' + }`}> + {classItem._count.students >= classItem.maxStudents + ? 'Penuh' + : classItem._count.students >= classItem.maxStudents * 0.8 + ? 'Hampir Penuh' + : 'Tersedia' + } + +
+
+
+ + +
+
+ )) + )} +
+ + {/* Add Class Modal */} + {showAddForm && ( +
+
+
+

Tambah Kelas Baru

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+
+
+ )} +
+
+ ) +} diff --git a/src/app/dashboard/parent/page.tsx b/src/app/dashboard/parent/page.tsx new file mode 100644 index 0000000..cca68f9 --- /dev/null +++ b/src/app/dashboard/parent/page.tsx @@ -0,0 +1,263 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import Image from 'next/image' + +interface User { + id: string + name: string + email: string + role: string +} + +interface Child { + id: string + name: string + studentNumber: string + class: string + attendance: number + lastGrade: string +} + +export default function ParentDashboard() { + const [user, setUser] = useState(null) + const [children] = useState([ + { + id: '1', + name: 'Ahmad Pratama', + studentNumber: 'SW001', + class: 'X IPA 1', + attendance: 95, + lastGrade: 'A' + } + ]) + const router = useRouter() + + useEffect(() => { + const token = localStorage.getItem('token') + const userData = localStorage.getItem('user') + + if (!token || !userData) { + router.push('/auth/login') + return + } + + const parsedUser = JSON.parse(userData) + if (parsedUser.role !== 'PARENT') { + router.push('/auth/login') + return + } + + setUser(parsedUser) + }, [router]) + + const handleLogout = () => { + localStorage.removeItem('token') + localStorage.removeItem('user') + router.push('/') + } + + if (!user) { + return
Loading...
+ } + + return ( +
+ {/* Header */} +
+
+
+
+ SIPINTAR Logo +

SIPINTAR

+ | + Dashboard Orang Tua +
+
+
+

{user.name}

+

Orang Tua

+
+ +
+
+
+
+ + {/* Main Content */} +
+ {/* Welcome Section */} +
+

+ Selamat Datang, {user.name}! +

+

Monitor perkembangan akademik anak Anda

+
+ + {/* Children Cards */} +
+ {children.map((child) => ( +
+
+
+

{child.name}

+

NIS: {child.studentNumber} | Kelas: {child.class}

+
+
+
+ + + +
+
+
+ + {/* Stats Grid */} +
+
+
+
+ + + +
+
+

Kehadiran

+

{child.attendance}%

+
+
+
+ +
+
+
+ + + +
+
+

Nilai Terakhir

+

{child.lastGrade}

+
+
+
+ +
+
+
+ + + +
+
+

Mata Pelajaran

+

12

+
+
+
+
+ + {/* Quick Actions for this child */} +
+ + + + + +
+
+ ))} +
+ + {/* Recent Updates */} +
+

Pemberitahuan Terbaru

+
+
+
+
+

Nilai Ujian Matematika

+

Ahmad Pratama mendapat nilai A untuk ujian matematika

+

2 jam yang lalu

+
+
+ +
+
+
+

Kehadiran Sempurna

+

Ahmad Pratama hadir tepat waktu hari ini

+

5 jam yang lalu

+
+
+ +
+
+
+

Pengumuman Sekolah

+

Rapat orang tua akan dilaksanakan minggu depan

+

1 hari yang lalu

+
+
+
+
+ + {/* Contact School */} +
+

Hubungi Sekolah

+
+
+
+ + + +
+
+

Telepon

+

+62 21 1234-5678

+
+
+ +
+
+ + + +
+
+

Email

+

info@sipintar.com

+
+
+
+
+
+
+ ) +} diff --git a/src/app/dashboard/students/page.tsx b/src/app/dashboard/students/page.tsx index 47f63ac..d3c9d17 100644 --- a/src/app/dashboard/students/page.tsx +++ b/src/app/dashboard/students/page.tsx @@ -21,6 +21,19 @@ export default function StudentsPage() { const [students, setStudents] = useState([]) const [isLoading, setIsLoading] = useState(true) const [showAddForm, setShowAddForm] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + studentNumber: '', + dateOfBirth: '', + address: '', + phone: '', + parentName: '', + parentPhone: '', + emergencyContact: '', + }) const router = useRouter() useEffect(() => { @@ -53,6 +66,61 @@ export default function StudentsPage() { } } + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsSubmitting(true) + + try { + const token = localStorage.getItem('token') + const response = await fetch('/api/students', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify(formData), + }) + + if (response.ok) { + // Reset form and close modal + setFormData({ + name: '', + email: '', + password: '', + studentNumber: '', + dateOfBirth: '', + address: '', + phone: '', + parentName: '', + parentPhone: '', + emergencyContact: '', + }) + setShowAddForm(false) + + // Refresh students list + await fetchStudents() + + alert('Siswa berhasil ditambahkan!') + } else { + const error = await response.json() + alert(error.message || 'Gagal menambahkan siswa') + } + } catch (error) { + console.error('Error adding student:', error) + alert('Terjadi kesalahan saat menambahkan siswa') + } finally { + setIsSubmitting(false) + } + } + if (isLoading) { return (
@@ -73,7 +141,7 @@ export default function StudentsPage() {
+
+
+

Tambah Siswa Baru

+ +
+
+ {/* Data Siswa */} +
+

Data Siswa

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {/* Data Orang Tua */} +
+

Data Orang Tua

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +