feat: Aplikasi SIPINTAR - Sistem Pemantauan Interaktif dan Pintar untuk Manajemen Sekolah
- Setup Next.js 15 dengan TypeScript dan Tailwind CSS - Implementasi Prisma ORM dengan MySQL database - Sistem autentikasi JWT untuk admin, guru, dan siswa - Dashboard dengan analytics dan chart data menggunakan Recharts - CRUD operations untuk siswa, guru, kelas, dan mata pelajaran - Sistem penilaian dan absensi - Landing page dengan branding SIPINTAR yang tepat - Database seeding dengan data demo - Dokumentasi setup database MySQL
This commit is contained in:
parent
c88b06c8fa
commit
d9392d0d1e
|
@ -0,0 +1,43 @@
|
|||
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
|
||||
|
||||
# SIPINTAR - Sistem Informasi Pintar Sekolah
|
||||
|
||||
Ini adalah aplikasi manajemen sekolah yang dibangun dengan Next.js, TypeScript, Prisma ORM, dan MySQL.
|
||||
|
||||
## Tech Stack
|
||||
- **Frontend**: Next.js 15, React, TypeScript, Tailwind CSS
|
||||
- **Backend**: Next.js API Routes
|
||||
- **Database**: MySQL dengan Prisma ORM
|
||||
- **Authentication**: NextAuth.js
|
||||
- **UI Components**: Radix UI, Lucide React
|
||||
- **Styling**: Tailwind CSS
|
||||
|
||||
## Struktur Database
|
||||
Aplikasi ini mengelola:
|
||||
- **Users**: Admin, Guru, dan Siswa dengan role-based access
|
||||
- **Students**: Data siswa lengkap dengan informasi orang tua
|
||||
- **Teachers**: Data guru dengan spesialisasi dan kualifikasi
|
||||
- **Subjects**: Mata pelajaran yang diajarkan
|
||||
- **Classes**: Kelas dengan wali kelas dan mata pelajaran
|
||||
- **Attendance**: Sistem absensi siswa
|
||||
- **Grades**: Sistem penilaian dengan berbagai jenis nilai
|
||||
- **Academic Years**: Manajemen tahun akademik
|
||||
|
||||
## Fitur Utama
|
||||
1. **Dashboard Admin**: Kelola seluruh sistem sekolah
|
||||
2. **Manajemen Siswa**: CRUD siswa, enrollment kelas
|
||||
3. **Manajemen Guru**: CRUD guru dan assignment mata pelajaran
|
||||
4. **Manajemen Kelas**: Pembagian kelas dan wali kelas
|
||||
5. **Sistem Absensi**: Input dan monitoring kehadiran
|
||||
6. **Sistem Penilaian**: Input nilai dan laporan akademik
|
||||
7. **Authentication**: Login role-based untuk Admin, Guru, Siswa
|
||||
|
||||
## Coding Guidelines
|
||||
- Gunakan TypeScript untuk type safety
|
||||
- Implementasikan proper error handling
|
||||
- Gunakan Prisma untuk database operations
|
||||
- Ikuti Next.js 15 App Router conventions
|
||||
- Gunakan server actions untuk form submissions
|
||||
- Implementasikan proper validation dengan Zod
|
||||
- Gunakan shadcn/ui design patterns
|
||||
- Responsive design dengan mobile-first approach
|
|
@ -39,3 +39,5 @@ yarn-error.log*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/src/generated/prisma
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
# 📊 Setup Database MySQL untuk SIPINTAR
|
||||
|
||||
## 🚀 Langkah Setup Database yang Sudah Berhasil
|
||||
|
||||
### ✅ Yang Sudah Selesai:
|
||||
1. **Database Schema**: Tabel-tabel sudah berhasil dibuat di MySQL
|
||||
2. **Data Demo**: Database sudah terisi dengan data demo
|
||||
3. **Konfigurasi**: File .env sudah dikonfigurasi dengan benar
|
||||
|
||||
### 🎯 Cara Setup Database dari Awal
|
||||
|
||||
#### **Opsi 1: Menggunakan XAMPP (Recommended untuk Development)**
|
||||
|
||||
1. **Download dan Install XAMPP**
|
||||
- Download dari: https://www.apachefriends.org/
|
||||
- Install dengan default settings
|
||||
|
||||
2. **Start Services**
|
||||
- Buka XAMPP Control Panel
|
||||
- Start **Apache** dan **MySQL**
|
||||
|
||||
3. **Buat Database via phpMyAdmin**
|
||||
- Buka http://localhost/phpmyadmin
|
||||
- Klik "New" di sidebar kiri
|
||||
- Database name: `sipintar_school`
|
||||
- Collation: `utf8mb4_unicode_ci`
|
||||
- Klik "Create"
|
||||
|
||||
#### **Opsi 2: Install MySQL Server Langsung**
|
||||
|
||||
1. **Download MySQL**
|
||||
- Download dari: https://dev.mysql.com/downloads/installer/
|
||||
- Pilih "MySQL Installer for Windows"
|
||||
|
||||
2. **Install MySQL**
|
||||
- Jalankan installer
|
||||
- Pilih "Developer Default"
|
||||
- Set root password (ingat password ini!)
|
||||
|
||||
3. **Buat Database**
|
||||
```sql
|
||||
# Login ke MySQL Command Line atau MySQL Workbench
|
||||
CREATE DATABASE sipintar_school CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### 🔧 Konfigurasi Aplikasi
|
||||
|
||||
1. **Edit file .env**
|
||||
```env
|
||||
# Jika menggunakan XAMPP (default tanpa password)
|
||||
DATABASE_URL="mysql://root:@localhost:3306/sipintar_school"
|
||||
|
||||
# Jika MySQL dengan password
|
||||
DATABASE_URL="mysql://root:your_password@localhost:3306/sipintar_school"
|
||||
|
||||
# Jika buat user khusus
|
||||
DATABASE_URL="mysql://sipintar_user:sipintar_password@localhost:3306/sipintar_school"
|
||||
```
|
||||
|
||||
2. **Push Schema ke Database**
|
||||
```bash
|
||||
npx prisma db push
|
||||
```
|
||||
|
||||
3. **Seed Database dengan Data Demo**
|
||||
```bash
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
### 🎯 Test Connection
|
||||
|
||||
Setelah setup, test dengan:
|
||||
|
||||
```bash
|
||||
# Test koneksi database
|
||||
npx prisma studio
|
||||
|
||||
# Atau jalankan development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 🔐 Demo Accounts yang Tersedia
|
||||
|
||||
Setelah seeding berhasil, gunakan akun berikut untuk login:
|
||||
|
||||
- **👨💼 Admin**:
|
||||
- Email: `admin@sipintar.com`
|
||||
- Password: `admin123`
|
||||
|
||||
- **👨🏫 Guru**:
|
||||
- Email: `guru@sipintar.com`
|
||||
- Password: `guru123`
|
||||
|
||||
- **👨🎓 Siswa**:
|
||||
- Email: `siswa@sipintar.com`
|
||||
- Password: `siswa123`
|
||||
|
||||
### 🗄️ Struktur Database yang Sudah Dibuat
|
||||
|
||||
Tabel-tabel berikut sudah otomatis dibuat:
|
||||
|
||||
```
|
||||
📊 Database: sipintar_school
|
||||
├── 👥 users (Base table untuk semua user)
|
||||
├── 👨🎓 students (Profile siswa)
|
||||
├── 👨🏫 teachers (Profile guru)
|
||||
├── 📚 subjects (Mata pelajaran)
|
||||
├── 🏫 classes (Kelas)
|
||||
├── 📝 class_students (Relasi siswa-kelas)
|
||||
├── ⏰ attendances (Absensi)
|
||||
├── 📊 grades (Nilai)
|
||||
└── 📅 academic_years (Tahun akademik)
|
||||
```
|
||||
|
||||
### 🛠️ Commands yang Berguna
|
||||
|
||||
```bash
|
||||
# Lihat database di browser
|
||||
npx prisma studio
|
||||
|
||||
# Reset database (hati-hati, akan hapus semua data!)
|
||||
npx prisma db push --force-reset
|
||||
|
||||
# Generate Prisma Client setelah perubahan schema
|
||||
npx prisma generate
|
||||
|
||||
# Seed ulang database
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
### ⚠️ Troubleshooting
|
||||
|
||||
**Error: "Can't connect to MySQL server"**
|
||||
- Pastikan MySQL service berjalan
|
||||
- Cek XAMPP Control Panel atau Windows Services
|
||||
|
||||
**Error: "Database doesn't exist"**
|
||||
- Buat database `sipintar_school` dulu via phpMyAdmin atau MySQL Command Line
|
||||
|
||||
**Error: "Access denied for user"**
|
||||
- Cek username/password di .env
|
||||
- Untuk XAMPP default: user=root, password=kosong
|
||||
|
||||
### ✅ Status Saat Ini
|
||||
|
||||
Database sudah berhasil disetup dengan:
|
||||
- ✅ 1 Admin user
|
||||
- ✅ 2 Teacher users
|
||||
- ✅ 2 Student users
|
||||
- ✅ 2 Subjects (Matematika, Bahasa Indonesia)
|
||||
- ✅ 2 Classes
|
||||
- ✅ Sample attendance records
|
||||
- ✅ Sample grades
|
||||
- ✅ Active academic year 2024/2025
|
||||
|
||||
**🎉 Aplikasi siap digunakan! Buka http://localhost:3000 dan login dengan demo accounts di atas.**
|
166
README.md
166
README.md
|
@ -1,24 +1,164 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# SIPINTAR - Sistem Informasi Pintar Sekolah
|
||||
|
||||
## Getting Started
|
||||
Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Prisma ORM, dan MySQL.
|
||||
|
||||
First, run the development server:
|
||||
## ✨ Fitur Utama
|
||||
|
||||
- 👨💼 **Dashboard Admin**: Kelola seluruh sistem sekolah
|
||||
- 👨🎓 **Manajemen Siswa**: CRUD siswa, enrollment kelas, data orang tua
|
||||
- 👨🏫 **Manajemen Guru**: CRUD guru, assignment mata pelajaran, kualifikasi
|
||||
- 🏫 **Manajemen Kelas**: Pembagian kelas, wali kelas, mata pelajaran
|
||||
- ⏰ **Sistem Absensi**: Input dan monitoring kehadiran realtime
|
||||
- 📊 **Sistem Penilaian**: Input nilai, laporan akademik, tracking progress
|
||||
- 🔐 **Authentication**: Login role-based (Admin, Guru, Siswa)
|
||||
- 📱 **Responsive Design**: Mobile-first approach
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Frontend**: Next.js 15, React, TypeScript, Tailwind CSS
|
||||
- **Backend**: Next.js API Routes
|
||||
- **Database**: MySQL dengan Prisma ORM
|
||||
- **Authentication**: JWT dengan bcryptjs
|
||||
- **UI Components**: Radix UI, Lucide React
|
||||
- **Styling**: Tailwind CSS
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- MySQL Database
|
||||
- npm/yarn/pnpm
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone repository** (jika dari git)
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
git clone <repository-url>
|
||||
cd sipintar-app
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
3. **Setup Database**
|
||||
- Buat database MySQL baru
|
||||
- Copy `.env.example` ke `.env`
|
||||
- Update connection string database di `.env`:
|
||||
```env
|
||||
DATABASE_URL="mysql://username:password@localhost:3306/sipintar_school"
|
||||
NEXTAUTH_SECRET="your-secret-key-here"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
```
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
4. **Setup Database Schema**
|
||||
```bash
|
||||
npx prisma db push
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
5. **Seed Database dengan Data Demo**
|
||||
```bash
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
6. **Jalankan Development Server**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
7. **Akses Aplikasi**
|
||||
Buka [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## 👥 Demo Accounts
|
||||
|
||||
Setelah menjalankan seeding, gunakan akun demo berikut:
|
||||
|
||||
- **Admin**: `admin@sipintar.com` / `admin123`
|
||||
- **Guru**: `guru@sipintar.com` / `guru123`
|
||||
- **Siswa**: `siswa@sipintar.com` / `siswa123`
|
||||
|
||||
## 📁 Struktur Database
|
||||
|
||||
### Users & Roles
|
||||
- **User**: Base table untuk semua pengguna
|
||||
- **Student**: Profile siswa dengan data orang tua
|
||||
- **Teacher**: Profile guru dengan spesialisasi
|
||||
|
||||
### Academic Management
|
||||
- **Subject**: Mata pelajaran yang diajarkan
|
||||
- **Class**: Kelas dengan wali kelas dan mata pelajaran
|
||||
- **ClassStudent**: Relasi many-to-many siswa dan kelas
|
||||
- **AcademicYear**: Tahun akademik
|
||||
|
||||
### Operations
|
||||
- **Attendance**: Sistem absensi harian
|
||||
- **Grade**: Sistem penilaian dengan berbagai jenis nilai
|
||||
|
||||
## 🔧 Available Scripts
|
||||
|
||||
```bash
|
||||
npm run dev # Jalankan development server
|
||||
npm run build # Build untuk production
|
||||
npm run start # Jalankan production server
|
||||
npm run lint # Jalankan ESLint
|
||||
npm run db:generate # Generate Prisma client
|
||||
npm run db:push # Push schema ke database
|
||||
npm run db:seed # Seed database dengan data demo
|
||||
```
|
||||
|
||||
## 📐 Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── api/ # API routes
|
||||
│ ├── auth/ # Authentication pages
|
||||
│ ├── dashboard/ # Dashboard pages
|
||||
│ └── page.tsx # Homepage
|
||||
├── lib/ # Utility functions
|
||||
│ ├── prisma.ts # Database connection
|
||||
│ ├── auth.ts # Authentication helpers
|
||||
│ └── utils.ts # General utilities
|
||||
└── components/ # React components
|
||||
|
||||
prisma/
|
||||
├── schema.prisma # Database schema
|
||||
└── seed.ts # Database seeding script
|
||||
```
|
||||
|
||||
## 🎯 Roadmap
|
||||
|
||||
- [ ] NextAuth.js integration
|
||||
- [ ] Email notifications
|
||||
- [ ] File upload untuk foto profil
|
||||
- [ ] Advanced reporting & analytics
|
||||
- [ ] Mobile app dengan React Native
|
||||
- [ ] Integration dengan sistem pembayaran
|
||||
- [ ] Multi-tenant support
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork repository
|
||||
2. Buat feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push branch (`git push origin feature/amazing-feature`)
|
||||
5. Open Pull Request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
Untuk bantuan dan dukungan:
|
||||
- Create issue di GitHub
|
||||
- Email: support@sipintar.com
|
||||
|
||||
---
|
||||
Dibuat dengan ❤️ menggunakan Next.js
|
||||
|
||||
## Learn More
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
|
@ -6,22 +6,46 @@
|
|||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^6.12.0",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.526.0",
|
||||
"mysql2": "^3.14.2",
|
||||
"next": "15.4.4",
|
||||
"next-auth": "^4.24.11",
|
||||
"prisma": "^6.12.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.4.4"
|
||||
"recharts": "^3.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// Model untuk User (Admin, Guru, Siswa)
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
password String
|
||||
role Role @default(STUDENT)
|
||||
profileImage String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
student Student?
|
||||
teacher Teacher?
|
||||
classEnrollments ClassStudent[]
|
||||
attendances Attendance[]
|
||||
grades Grade[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
ADMIN
|
||||
TEACHER
|
||||
STUDENT
|
||||
}
|
||||
|
||||
// Model untuk Siswa
|
||||
model Student {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
studentNumber String @unique
|
||||
dateOfBirth DateTime
|
||||
address String
|
||||
phone String?
|
||||
parentName String
|
||||
parentPhone String
|
||||
emergencyContact String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
classEnrollments ClassStudent[]
|
||||
attendances Attendance[]
|
||||
grades Grade[]
|
||||
|
||||
@@map("students")
|
||||
}
|
||||
|
||||
// Model untuk Guru
|
||||
model Teacher {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
teacherNumber String @unique
|
||||
specialization String
|
||||
qualification String
|
||||
experience Int // dalam tahun
|
||||
phone String?
|
||||
address String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
subjects Subject[]
|
||||
classes Class[]
|
||||
attendances Attendance[]
|
||||
grades Grade[]
|
||||
|
||||
@@map("teachers")
|
||||
}
|
||||
|
||||
// Model untuk Mata Pelajaran
|
||||
model Subject {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
code String @unique
|
||||
description String?
|
||||
credits Int @default(1)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
teacherId String
|
||||
teacher Teacher @relation(fields: [teacherId], references: [id])
|
||||
classes Class[]
|
||||
grades Grade[]
|
||||
|
||||
@@map("subjects")
|
||||
}
|
||||
|
||||
// Model untuk Kelas
|
||||
model Class {
|
||||
id String @id @default(cuid())
|
||||
name String // e.g., "X-IPA-1", "XI-IPS-2"
|
||||
grade String // e.g., "X", "XI", "XII"
|
||||
section String // e.g., "IPA", "IPS"
|
||||
maxStudents Int @default(30)
|
||||
room String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
teacherId String // Wali kelas
|
||||
teacher Teacher @relation(fields: [teacherId], references: [id])
|
||||
subjectId String
|
||||
subject Subject @relation(fields: [subjectId], references: [id])
|
||||
students ClassStudent[]
|
||||
attendances Attendance[]
|
||||
grades Grade[]
|
||||
|
||||
@@map("classes")
|
||||
}
|
||||
|
||||
// Model untuk hubungan Siswa dan Kelas (Many-to-Many)
|
||||
model ClassStudent {
|
||||
id String @id @default(cuid())
|
||||
studentId String
|
||||
classId String
|
||||
userId String
|
||||
enrolledAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
|
||||
// Relations
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([studentId, classId])
|
||||
@@map("class_students")
|
||||
}
|
||||
|
||||
// Model untuk Absensi
|
||||
model Attendance {
|
||||
id String @id @default(cuid())
|
||||
date DateTime
|
||||
status AttendanceStatus
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
teacherId String
|
||||
teacher Teacher @relation(fields: [teacherId], references: [id])
|
||||
classId String
|
||||
class Class @relation(fields: [classId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([studentId, classId, date])
|
||||
@@map("attendances")
|
||||
}
|
||||
|
||||
enum AttendanceStatus {
|
||||
PRESENT
|
||||
ABSENT
|
||||
LATE
|
||||
EXCUSED
|
||||
}
|
||||
|
||||
// Model untuk Nilai
|
||||
model Grade {
|
||||
id String @id @default(cuid())
|
||||
type GradeType
|
||||
score Float
|
||||
maxScore Float @default(100)
|
||||
description String?
|
||||
date DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
studentId String
|
||||
student Student @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
teacherId String
|
||||
teacher Teacher @relation(fields: [teacherId], references: [id])
|
||||
subjectId String
|
||||
subject Subject @relation(fields: [subjectId], references: [id])
|
||||
classId String
|
||||
class Class @relation(fields: [classId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("grades")
|
||||
}
|
||||
|
||||
enum GradeType {
|
||||
QUIZ
|
||||
ASSIGNMENT
|
||||
MIDTERM
|
||||
FINAL
|
||||
PROJECT
|
||||
}
|
||||
|
||||
// Model untuk Tahun Akademik
|
||||
model AcademicYear {
|
||||
id String @id @default(cuid())
|
||||
name String @unique // e.g., "2024/2025"
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
isActive Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("academic_years")
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
import { PrismaClient } from '@prisma/client'
|
||||
import { hashPassword } from '../src/lib/auth'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Starting database seeding...')
|
||||
|
||||
// Create Admin User
|
||||
const adminPassword = await hashPassword('admin123')
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email: 'admin@sipintar.com',
|
||||
name: 'Administrator',
|
||||
password: adminPassword,
|
||||
role: 'ADMIN',
|
||||
},
|
||||
})
|
||||
|
||||
// Create Teacher Users and Teacher profiles
|
||||
const teacherPassword = await hashPassword('guru123')
|
||||
|
||||
const teacher1 = await prisma.user.create({
|
||||
data: {
|
||||
email: 'guru@sipintar.com',
|
||||
name: 'Budi Santoso',
|
||||
password: teacherPassword,
|
||||
role: 'TEACHER',
|
||||
teacher: {
|
||||
create: {
|
||||
teacherNumber: 'GR001',
|
||||
specialization: 'Matematika',
|
||||
qualification: 'S1 Pendidikan Matematika',
|
||||
experience: 10,
|
||||
phone: '081234567890',
|
||||
address: 'Jl. Pendidikan No. 1, Jakarta',
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { teacher: true },
|
||||
})
|
||||
|
||||
const teacher2 = await prisma.user.create({
|
||||
data: {
|
||||
email: 'sari.guru@sipintar.com',
|
||||
name: 'Sari Dewi',
|
||||
password: teacherPassword,
|
||||
role: 'TEACHER',
|
||||
teacher: {
|
||||
create: {
|
||||
teacherNumber: 'GR002',
|
||||
specialization: 'Bahasa Indonesia',
|
||||
qualification: 'S1 Pendidikan Bahasa Indonesia',
|
||||
experience: 8,
|
||||
phone: '081234567891',
|
||||
address: 'Jl. Bahasa No. 2, Jakarta',
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { teacher: true },
|
||||
})
|
||||
|
||||
// Create Student User and Student profile
|
||||
const studentPassword = await hashPassword('siswa123')
|
||||
const student1 = await prisma.user.create({
|
||||
data: {
|
||||
email: 'siswa@sipintar.com',
|
||||
name: 'Ahmad Fauzi',
|
||||
password: studentPassword,
|
||||
role: 'STUDENT',
|
||||
student: {
|
||||
create: {
|
||||
studentNumber: 'SW001',
|
||||
dateOfBirth: new Date('2008-05-15'),
|
||||
address: 'Jl. Siswa No. 1, Jakarta',
|
||||
phone: '081234567892',
|
||||
parentName: 'Ali Rahman',
|
||||
parentPhone: '081234567893',
|
||||
emergencyContact: '081234567894',
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { student: true },
|
||||
})
|
||||
|
||||
const student2 = await prisma.user.create({
|
||||
data: {
|
||||
email: 'rina.siswa@sipintar.com',
|
||||
name: 'Rina Sari',
|
||||
password: studentPassword,
|
||||
role: 'STUDENT',
|
||||
student: {
|
||||
create: {
|
||||
studentNumber: 'SW002',
|
||||
dateOfBirth: new Date('2008-08-20'),
|
||||
address: 'Jl. Pelajar No. 2, Jakarta',
|
||||
phone: '081234567895',
|
||||
parentName: 'Joko Widodo',
|
||||
parentPhone: '081234567896',
|
||||
emergencyContact: '081234567897',
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { student: true },
|
||||
})
|
||||
|
||||
// Create Subjects
|
||||
const mathSubject = await prisma.subject.create({
|
||||
data: {
|
||||
name: 'Matematika',
|
||||
code: 'MTK101',
|
||||
description: 'Matematika untuk kelas X',
|
||||
credits: 4,
|
||||
teacherId: teacher1.teacher!.id,
|
||||
},
|
||||
})
|
||||
|
||||
const indonesianSubject = await prisma.subject.create({
|
||||
data: {
|
||||
name: 'Bahasa Indonesia',
|
||||
code: 'BIN101',
|
||||
description: 'Bahasa Indonesia untuk kelas X',
|
||||
credits: 3,
|
||||
teacherId: teacher2.teacher!.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Create Classes
|
||||
const class1 = await prisma.class.create({
|
||||
data: {
|
||||
name: 'X-IPA-1',
|
||||
grade: 'X',
|
||||
section: 'IPA',
|
||||
maxStudents: 30,
|
||||
room: 'R101',
|
||||
teacherId: teacher1.teacher!.id, // Wali kelas
|
||||
subjectId: mathSubject.id,
|
||||
},
|
||||
})
|
||||
|
||||
const class2 = await prisma.class.create({
|
||||
data: {
|
||||
name: 'X-IPA-1',
|
||||
grade: 'X',
|
||||
section: 'IPA',
|
||||
maxStudents: 30,
|
||||
room: 'R102',
|
||||
teacherId: teacher2.teacher!.id, // Wali kelas
|
||||
subjectId: indonesianSubject.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Enroll students in classes
|
||||
await prisma.classStudent.create({
|
||||
data: {
|
||||
studentId: student1.student!.id,
|
||||
classId: class1.id,
|
||||
userId: student1.id,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.classStudent.create({
|
||||
data: {
|
||||
studentId: student1.student!.id,
|
||||
classId: class2.id,
|
||||
userId: student1.id,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.classStudent.create({
|
||||
data: {
|
||||
studentId: student2.student!.id,
|
||||
classId: class1.id,
|
||||
userId: student2.id,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.classStudent.create({
|
||||
data: {
|
||||
studentId: student2.student!.id,
|
||||
classId: class2.id,
|
||||
userId: student2.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Create sample attendance records
|
||||
const today = new Date()
|
||||
await prisma.attendance.create({
|
||||
data: {
|
||||
date: today,
|
||||
status: 'PRESENT',
|
||||
studentId: student1.student!.id,
|
||||
teacherId: teacher1.teacher!.id,
|
||||
classId: class1.id,
|
||||
userId: student1.id,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.attendance.create({
|
||||
data: {
|
||||
date: today,
|
||||
status: 'PRESENT',
|
||||
studentId: student2.student!.id,
|
||||
teacherId: teacher1.teacher!.id,
|
||||
classId: class1.id,
|
||||
userId: student2.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Create sample grades
|
||||
await prisma.grade.create({
|
||||
data: {
|
||||
type: 'QUIZ',
|
||||
score: 85,
|
||||
maxScore: 100,
|
||||
description: 'Quiz Matematika Bab 1',
|
||||
date: today,
|
||||
studentId: student1.student!.id,
|
||||
teacherId: teacher1.teacher!.id,
|
||||
subjectId: mathSubject.id,
|
||||
classId: class1.id,
|
||||
userId: student1.id,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.grade.create({
|
||||
data: {
|
||||
type: 'ASSIGNMENT',
|
||||
score: 90,
|
||||
maxScore: 100,
|
||||
description: 'Tugas Bahasa Indonesia',
|
||||
date: today,
|
||||
studentId: student1.student!.id,
|
||||
teacherId: teacher2.teacher!.id,
|
||||
subjectId: indonesianSubject.id,
|
||||
classId: class2.id,
|
||||
userId: student1.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Create Academic Year
|
||||
await prisma.academicYear.create({
|
||||
data: {
|
||||
name: '2024/2025',
|
||||
startDate: new Date('2024-07-01'),
|
||||
endDate: new Date('2025-06-30'),
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('✅ Database seeding completed!')
|
||||
console.log('\n📝 Demo accounts created:')
|
||||
console.log('👨💼 Admin: admin@sipintar.com / admin123')
|
||||
console.log('👨🏫 Guru: guru@sipintar.com / guru123')
|
||||
console.log('👨🎓 Siswa: siswa@sipintar.com / siswa123')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Seeding failed:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
# Login ke MySQL
|
||||
mysql -u root -p
|
||||
|
||||
# Buat database baru
|
||||
CREATE DATABASE sipintar_school CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
# Buat user khusus (opsional tapi recommended)
|
||||
CREATE USER 'sipintar_user'@'localhost' IDENTIFIED BY 'sipintar_password123';
|
||||
|
||||
# Berikan akses ke database
|
||||
GRANT ALL PRIVILEGES ON sipintar_school.* TO 'sipintar_user'@'localhost';
|
||||
|
||||
# Refresh privileges
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
# Keluar dari MySQL
|
||||
EXIT;
|
|
@ -0,0 +1,78 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { verifyPassword } from '@/lib/auth'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, password } = await request.json()
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Email and password are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: {
|
||||
student: true,
|
||||
teacher: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await verifyPassword(password, user.password)
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
name: user.name,
|
||||
},
|
||||
process.env.NEXTAUTH_SECRET || 'fallback-secret',
|
||||
{ expiresIn: '7d' }
|
||||
)
|
||||
|
||||
// Prepare user data for response
|
||||
const userData = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
profileImage: user.profileImage,
|
||||
...(user.student && { studentId: user.student.id, studentNumber: user.student.studentNumber }),
|
||||
...(user.teacher && { teacherId: user.teacher.id, teacherNumber: user.teacher.teacherNumber }),
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
user: userData,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1]
|
||||
jwt.verify(token, process.env.NEXTAUTH_SECRET || 'sipintar-secret-key-2025-change-in-production')
|
||||
|
||||
// Sample data untuk demo - nanti bisa diganti dengan data real dari database
|
||||
const enrollmentData = [
|
||||
{ name: 'Kelas X-1', students: 32 },
|
||||
{ name: 'Kelas X-2', students: 30 },
|
||||
{ name: 'Kelas XI-1', students: 28 },
|
||||
{ name: 'Kelas XI-2', students: 29 },
|
||||
{ name: 'Kelas XII-1', students: 25 },
|
||||
{ name: 'Kelas XII-2', students: 27 }
|
||||
]
|
||||
|
||||
const attendanceData = [
|
||||
{ month: 'Jan 2025', present: 850, absent: 45, late: 23 },
|
||||
{ month: 'Feb 2025', present: 823, absent: 52, late: 31 },
|
||||
{ month: 'Mar 2025', present: 867, absent: 38, late: 19 },
|
||||
{ month: 'Apr 2025', present: 834, absent: 47, late: 25 },
|
||||
{ month: 'May 2025', present: 856, absent: 41, late: 22 },
|
||||
{ month: 'Jun 2025', present: 878, absent: 35, late: 18 }
|
||||
]
|
||||
|
||||
const gradeData = [
|
||||
{ grade: 'A', count: 45 },
|
||||
{ grade: 'B', count: 78 },
|
||||
{ grade: 'C', count: 123 },
|
||||
{ grade: 'D', count: 34 },
|
||||
{ grade: 'E', count: 12 }
|
||||
]
|
||||
|
||||
const subjectData = [
|
||||
{ name: 'Matematika', teachers: 4 },
|
||||
{ name: 'Bahasa Indonesia', teachers: 3 },
|
||||
{ name: 'Bahasa Inggris', teachers: 3 },
|
||||
{ name: 'Fisika', teachers: 2 },
|
||||
{ name: 'Kimia', teachers: 2 },
|
||||
{ name: 'Biologi', teachers: 2 },
|
||||
{ name: 'Sejarah', teachers: 2 },
|
||||
{ name: 'Geografi', teachers: 1 }
|
||||
]
|
||||
|
||||
return NextResponse.json({
|
||||
enrollmentData,
|
||||
attendanceData,
|
||||
gradeData,
|
||||
subjectData
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Charts API error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
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
|
||||
}
|
||||
|
||||
if (decoded.role !== 'ADMIN') {
|
||||
return NextResponse.json(
|
||||
{ message: 'Access denied. Admin role required.' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get dashboard statistics
|
||||
const [totalStudents, totalTeachers, totalClasses, totalSubjects] = await Promise.all([
|
||||
prisma.student.count({ where: { user: { isActive: true } } }),
|
||||
prisma.teacher.count({ where: { user: { isActive: true } } }),
|
||||
prisma.class.count({ where: { isActive: true } }),
|
||||
prisma.subject.count({ where: { isActive: true } }),
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
totalStudents,
|
||||
totalTeachers,
|
||||
totalClasses,
|
||||
totalSubjects,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Dashboard stats error:', error)
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
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 students with user information
|
||||
const students = await prisma.student.findMany({
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
classEnrollments: {
|
||||
include: {
|
||||
class: {
|
||||
select: {
|
||||
name: true,
|
||||
grade: true,
|
||||
section: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
studentNumber: 'asc',
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json(students)
|
||||
} catch (error) {
|
||||
console.error('Get students 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,
|
||||
studentNumber,
|
||||
dateOfBirth,
|
||||
address,
|
||||
phone,
|
||||
parentName,
|
||||
parentPhone,
|
||||
emergencyContact,
|
||||
} = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !email || !password || !studentNumber || !dateOfBirth || !address || !parentName || !parentPhone) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if email or student number already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Email already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const existingStudent = await prisma.student.findUnique({
|
||||
where: { studentNumber },
|
||||
})
|
||||
|
||||
if (existingStudent) {
|
||||
return NextResponse.json(
|
||||
{ message: 'Student number already exists' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
// Create user and student in a transaction
|
||||
const result = await prisma.$transaction(async (tx: import('@prisma/client').Prisma.TransactionClient) => {
|
||||
// Create user
|
||||
const user = await tx.user.create({
|
||||
data: {
|
||||
email,
|
||||
name,
|
||||
password: hashedPassword,
|
||||
role: 'STUDENT',
|
||||
},
|
||||
})
|
||||
|
||||
// Create student profile
|
||||
const student = await tx.student.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
studentNumber,
|
||||
dateOfBirth: new Date(dateOfBirth),
|
||||
address,
|
||||
phone,
|
||||
parentName,
|
||||
parentPhone,
|
||||
emergencyContact,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return student
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Student created successfully',
|
||||
student: result,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Create student error:', error)
|
||||
return NextResponse.json(
|
||||
{ message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// Store token or handle authentication
|
||||
localStorage.setItem('token', data.token)
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
const data = await response.json()
|
||||
setError(data.message || 'Login failed')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err)
|
||||
setError('An error occurred. Please try again.')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
<div className="max-w-md w-full space-y-8 p-8">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold text-gray-900">SIPINTAR</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">Sistem Informasi Pintar Sekolah</p>
|
||||
<h3 className="mt-6 text-xl font-semibold text-gray-900">Masuk ke Akun Anda</h3>
|
||||
</div>
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-md text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
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"
|
||||
placeholder="Masukkan email Anda"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
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"
|
||||
placeholder="Masukkan password Anda"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
|
||||
Ingat saya
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
Lupa password?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? 'Masuk...' : 'Masuk'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
Belum punya akun?{' '}
|
||||
<Link href="/auth/register" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
Daftar di sini
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Demo Accounts */}
|
||||
<div className="mt-8 p-4 bg-gray-50 rounded-md">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">Demo Accounts:</h4>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<p><strong>Admin:</strong> admin@sipintar.com / admin123</p>
|
||||
<p><strong>Guru:</strong> guru@sipintar.com / guru123</p>
|
||||
<p><strong>Siswa:</strong> siswa@sipintar.com / siswa123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,428 @@
|
|||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Navigation */}
|
||||
<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</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">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
Selamat datang, {user?.name}!
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
{user?.role === 'ADMIN' && 'Kelola sistem sekolah dari dashboard admin.'}
|
||||
{user?.role === 'TEACHER' && 'Kelola kelas, siswa, dan nilai dari dashboard guru.'}
|
||||
{user?.role === 'STUDENT' && 'Lihat jadwal, nilai, dan absensi Anda.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
{user?.role === 'ADMIN' && stats && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Charts Section */}
|
||||
{user?.role === 'ADMIN' && chartData && (
|
||||
<div className="mb-8">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-6">Analytics Dashboard</h3>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Student Enrollment by Class */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Jumlah Siswa per Kelas</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData.enrollmentData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="students" fill="#3B82F6" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Grade Distribution */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Distribusi Nilai</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData.gradeData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name}: ${((percent ?? 0) * 100).toFixed(0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="count"
|
||||
>
|
||||
{chartData.gradeData.map((entry, index) => {
|
||||
const colors = ['#10B981', '#3B82F6', '#F59E0B', '#EF4444', '#6B7280']
|
||||
return <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
||||
})}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Attendance Trend */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Tren Kehadiran (6 Bulan Terakhir)</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={chartData.attendanceData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="present" stroke="#10B981" name="Hadir" />
|
||||
<Line type="monotone" dataKey="absent" stroke="#EF4444" name="Tidak Hadir" />
|
||||
<Line type="monotone" dataKey="late" stroke="#F59E0B" name="Terlambat" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Teachers per Subject */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Guru per Mata Pelajaran</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData.subjectData} layout="horizontal">
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" />
|
||||
<YAxis type="category" dataKey="name" width={100} />
|
||||
<Tooltip />
|
||||
<Bar dataKey="teachers" fill="#8B5CF6" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{user?.role === 'ADMIN' && (
|
||||
<>
|
||||
<Link href="/dashboard/students" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelola Siswa</h3>
|
||||
<p className="text-sm text-gray-600">Tambah, edit, dan kelola data siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/teachers" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelola Guru</h3>
|
||||
<p className="text-sm text-gray-600">Tambah, edit, dan kelola data guru</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/classes" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelola Kelas</h3>
|
||||
<p className="text-sm text-gray-600">Atur kelas dan mata pelajaran</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{user?.role === 'TEACHER' && (
|
||||
<>
|
||||
<Link href="/dashboard/my-classes" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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="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">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelas Saya</h3>
|
||||
<p className="text-sm text-gray-600">Lihat dan kelola kelas yang Anda ajar</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/attendance" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Absensi</h3>
|
||||
<p className="text-sm text-gray-600">Input dan kelola absensi siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/grades" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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="M9 19v-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>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Nilai</h3>
|
||||
<p className="text-sm text-gray-600">Input dan kelola nilai siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{user?.role === 'STUDENT' && (
|
||||
<>
|
||||
<Link href="/dashboard/my-grades" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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="M9 19v-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>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Nilai Saya</h3>
|
||||
<p className="text-sm text-gray-600">Lihat nilai dan progress akademik</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/my-attendance" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Absensi Saya</h3>
|
||||
<p className="text-sm text-gray-600">Lihat riwayat kehadiran</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/schedule" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Jadwal</h3>
|
||||
<p className="text-sm text-gray-600">Lihat jadwal pelajaran</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface Student {
|
||||
id: string
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
studentNumber: string
|
||||
dateOfBirth: string
|
||||
address: string
|
||||
phone?: string
|
||||
parentName: string
|
||||
parentPhone: string
|
||||
}
|
||||
|
||||
export default function StudentsPage() {
|
||||
const [students, setStudents] = useState<Student[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
router.push('/auth/login')
|
||||
return
|
||||
}
|
||||
|
||||
fetchStudents()
|
||||
}, [router])
|
||||
|
||||
const fetchStudents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch('/api/students', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setStudents(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch students:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Navigation */}
|
||||
<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">Manajemen Siswa</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => router.push('/dashboard')}
|
||||
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
← Kembali ke Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Manajemen Siswa</h2>
|
||||
<p className="text-gray-600">Kelola data siswa dan informasi akademik</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
+ Tambah Siswa
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Students Table */}
|
||||
<div className="bg-white shadow-sm rounded-lg border">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900">Daftar Siswa</h3>
|
||||
</div>
|
||||
|
||||
{students.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" 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>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">Belum ada siswa</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Mulai dengan menambahkan siswa baru.</p>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
+ Tambah Siswa Pertama
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Siswa
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
NIS
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Kontak
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Orang Tua
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Aksi
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{students.map((student) => (
|
||||
<tr key={student.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{student.user.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{student.user.email}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{student.studentNumber}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<div>{student.phone || 'Tidak ada'}</div>
|
||||
<div className="text-xs">{student.address}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<div>{student.parentName}</div>
|
||||
<div className="text-xs">{student.parentPhone}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button className="text-blue-600 hover:text-blue-900 mr-3">
|
||||
Edit
|
||||
</button>
|
||||
<button className="text-red-600 hover:text-red-900">
|
||||
Hapus
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Student Modal */}
|
||||
{showAddForm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Tambah Siswa Baru</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Fitur ini akan segera tersedia. Gunakan API endpoint /api/students untuk menambah siswa.
|
||||
</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
|
||||
>
|
||||
Tutup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
{/* Navigation */}
|
||||
<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">Sistem Informasi Pintar Sekolah</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Daftar
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="text-center">
|
||||
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
|
||||
Selamat Datang di <span className="text-blue-600">SIPINTAR</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
|
||||
Sistem Informasi Pintar untuk manajemen sekolah yang komprehensif.
|
||||
Kelola siswa, guru, kelas, absensi, dan nilai dengan mudah dan efisien.
|
||||
</p>
|
||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="rounded-md bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Mulai Sekarang
|
||||
</Link>
|
||||
<a href="#features" className="text-sm font-semibold leading-6 text-gray-900">
|
||||
Pelajari lebih lanjut <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div id="features" className="mt-20">
|
||||
<div className="text-center">
|
||||
<h3 className="text-3xl font-bold tracking-tight text-gray-900">Fitur Unggulan</h3>
|
||||
<p className="mt-4 text-lg text-gray-600">
|
||||
Kelola seluruh aspek sekolah dengan satu platform terintegrasi
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Manajemen Siswa</h4>
|
||||
<p className="mt-2 text-gray-600">Kelola data siswa, enrollment kelas, dan informasi orang tua dengan mudah.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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="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>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Manajemen Kelas</h4>
|
||||
<p className="mt-2 text-gray-600">Atur kelas, mata pelajaran, dan jadwal dengan sistem yang terintegrasi.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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="M9 19v-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>
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Sistem Penilaian</h4>
|
||||
<p className="mt-2 text-gray-600">Input nilai, generate laporan, dan tracking progress akademik siswa.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-yellow-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Absensi Digital</h4>
|
||||
<p className="mt-2 text-gray-600">Sistem absensi digital dengan laporan kehadiran realtime.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-red-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>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Manajemen Guru</h4>
|
||||
<p className="mt-2 text-gray-600">Kelola data guru, assignment mata pelajaran, dan kualifikasi.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Dashboard Admin</h4>
|
||||
<p className="mt-2 text-gray-600">Dashboard lengkap dengan analytics dan monitoring sistem sekolah.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-900 text-white py-12 mt-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-bold">SIPINTAR</h3>
|
||||
<p className="mt-2 text-gray-400">Sistem Informasi Pintar Sekolah</p>
|
||||
<p className="mt-4 text-sm text-gray-500">
|
||||
© 2025 SIPINTAR. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
221
src/app/page.tsx
221
src/app/page.tsx
|
@ -1,102 +1,141 @@
|
|||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
{/* Navigation */}
|
||||
<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">Sistem Pemantauan Interaktif dan Pintar</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Daftar
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{/* Hero Section */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="text-center">
|
||||
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
|
||||
Selamat Datang di <span className="text-blue-600">SIPINTAR</span>
|
||||
</h2>
|
||||
<p className="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
|
||||
Sistem Pemantauan Interaktif dan Pintar untuk manajemen sekolah yang komprehensif.
|
||||
Kelola siswa, guru, kelas, absensi, dan nilai dengan mudah, efisien, dan monitoring real-time.
|
||||
</p>
|
||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="rounded-md bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
Mulai Sekarang
|
||||
</Link>
|
||||
<a href="#features" className="text-sm font-semibold leading-6 text-gray-900">
|
||||
Pelajari lebih lanjut <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div id="features" className="mt-20">
|
||||
<div className="text-center">
|
||||
<h3 className="text-3xl font-bold tracking-tight text-gray-900">Fitur Unggulan</h3>
|
||||
<p className="mt-4 text-lg text-gray-600">
|
||||
Pantau dan kelola seluruh aspek sekolah dengan sistem pemantauan interaktif dan pintar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Siswa</h4>
|
||||
<p className="mt-2 text-gray-600">Pantau dan kelola data siswa, enrollment kelas, dan progress akademik secara real-time.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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="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>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Kelas</h4>
|
||||
<p className="mt-2 text-gray-600">Pantau aktivitas kelas, mata pelajaran, dan jadwal dengan sistem monitoring interaktif.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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="M9 19v-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>
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Sistem Penilaian Pintar</h4>
|
||||
<p className="mt-2 text-gray-600">Input nilai, generate laporan otomatis, dan tracking progress akademik dengan analitik pintar.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-yellow-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Kehadiran</h4>
|
||||
<p className="mt-2 text-gray-600">Sistem absensi digital dengan laporan kehadiran dan alert otomatis real-time.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-red-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>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Guru</h4>
|
||||
<p className="mt-2 text-gray-600">Pantau kinerja guru, assignment mata pelajaran, dan evaluasi kualifikasi secara interaktif.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">Dashboard Pemantauan Pintar</h4>
|
||||
<p className="mt-2 text-gray-600">Dashboard interaktif dengan analytics, monitoring, dan insights pintar untuk sistem sekolah.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-900 text-white py-12 mt-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-bold">SIPINTAR</h3>
|
||||
<p className="mt-2 text-gray-400">Sistem Pemantauan Interaktif dan Pintar</p>
|
||||
<p className="mt-4 text-sm text-gray-500">
|
||||
© 2025 SIPINTAR. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import bcrypt from 'bcryptjs'
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return await bcrypt.hash(password, 12)
|
||||
}
|
||||
|
||||
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
return await bcrypt.compare(password, hashedPassword)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
Loading…
Reference in New Issue