Compare commits
10 Commits
d9392d0d1e
...
13d26079da
| Author | SHA1 | Date |
|---|---|---|
|
|
13d26079da | |
|
|
892619df08 | |
|
|
4b1f6d6362 | |
|
|
72f6fea737 | |
|
|
5db127dfd1 | |
|
|
9dd8e4b9bd | |
|
|
959b7eea03 | |
|
|
f67578e830 | |
|
|
553e8bc8a3 | |
|
|
59bb5ef563 |
|
|
@ -0,0 +1,49 @@
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
.env.prod
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
/src/generated/prisma
|
||||||
|
|
||||||
|
# secrets
|
||||||
|
/secrets/
|
||||||
|
|
||||||
|
#security scanning
|
||||||
|
/trivy-results/
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
DATABASE_URL="mysql://sipintar_user:sipintar_password123:@db:3306/sipintar_school"
|
||||||
|
# DATABASE_URL="mysql://root:root123@sipintar-mysql:3306/sipintar_school"
|
||||||
|
NEXTAUTH_SECRET="your-secret-key-here-change-this-in-production"
|
||||||
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#DATABASE_URL="mysql://root:@localhost:3306/sipintar_school"
|
||||||
|
#NEXTAUTH_SECRET="your-secret-key-here-change-this-in-production"
|
||||||
|
#NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
|
@ -24,15 +24,17 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
.env.prod
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
|
||||||
.env*
|
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
|
@ -41,3 +43,9 @@ yarn-error.log*
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
/src/generated/prisma
|
/src/generated/prisma
|
||||||
|
|
||||||
|
#secrets
|
||||||
|
/secrets/
|
||||||
|
|
||||||
|
#trivy
|
||||||
|
/trivy-results/
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Generate Prisma Client
|
||||||
|
RUN npx prisma generate
|
||||||
|
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
#buat user baru untuk security setup
|
||||||
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# change ownership untuk user baru
|
||||||
|
RUN chown -R appuser:appgroup /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN pnpm run build
|
||||||
|
RUN chown -R appuser:appgroup /app/node_modules
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
#buat user di runner stage
|
||||||
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY --from=builder /app/.next ./.next
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/src ./src
|
||||||
|
COPY --chown=appuser:appgroup --from=builder /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
#ganti user jadi non root
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
20
README.md
20
README.md
|
|
@ -1,17 +1,20 @@
|
||||||
# SIPINTAR - Sistem Informasi Pintar Sekolah
|
# SIPINTAR - Sistem Pemantauan Interaktif dan Pintar
|
||||||
|
<img width="1342" height="632" alt="Screenshot 2025-08-08 215857" src="https://github.com/user-attachments/assets/22c2c735-c1b8-41be-ab57-3ab2ecc8dfe4" />
|
||||||
|
<img width="1346" height="630" alt="Screenshot 2025-08-08 213859" src="https://github.com/user-attachments/assets/fa544aea-8a47-43f3-96a7-e3996bedd95c" />
|
||||||
|
|
||||||
Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Prisma ORM, dan MySQL.
|
Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Prisma ORM, dan MySQL.
|
||||||
|
|
||||||
## ✨ Fitur Utama
|
## ✨ Fitur Utama
|
||||||
|
|
||||||
- 👨💼 **Dashboard Admin**: Kelola seluruh sistem sekolah
|
- 👨💼 **Dashboard Pemantauan Pintar**: Kelola seluruh sistem sekolah dengan analytics
|
||||||
- 👨🎓 **Manajemen Siswa**: CRUD siswa, enrollment kelas, data orang tua
|
- 👨🎓 **Pemantauan Siswa**: CRUD siswa, enrollment kelas, tracking progress real-time
|
||||||
- 👨🏫 **Manajemen Guru**: CRUD guru, assignment mata pelajaran, kualifikasi
|
- 👨🏫 **Pemantauan Guru**: CRUD guru, assignment mata pelajaran, evaluasi kinerja
|
||||||
- 🏫 **Manajemen Kelas**: Pembagian kelas, wali kelas, mata pelajaran
|
- 🏫 **Pemantauan Kelas**: Monitoring aktivitas kelas, wali kelas, mata pelajaran
|
||||||
- ⏰ **Sistem Absensi**: Input dan monitoring kehadiran realtime
|
- ⏰ **Sistem Absensi Interaktif**: Input dan monitoring kehadiran dengan alert otomatis
|
||||||
- 📊 **Sistem Penilaian**: Input nilai, laporan akademik, tracking progress
|
- 📊 **Sistem Penilaian Pintar**: Input nilai, analitik akademik, insights progress
|
||||||
- 🔐 **Authentication**: Login role-based (Admin, Guru, Siswa)
|
- 🔐 **Authentication**: Login role-based (Admin, Guru, Siswa)
|
||||||
- 📱 **Responsive Design**: Mobile-first approach
|
- 📱 **Responsive Design**: Mobile-first approach
|
||||||
|
- 📈 **Analytics Dashboard**: Chart dan visualisasi data dengan Recharts
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
|
@ -20,6 +23,7 @@ Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Pris
|
||||||
- **Database**: MySQL dengan Prisma ORM
|
- **Database**: MySQL dengan Prisma ORM
|
||||||
- **Authentication**: JWT dengan bcryptjs
|
- **Authentication**: JWT dengan bcryptjs
|
||||||
- **UI Components**: Radix UI, Lucide React
|
- **UI Components**: Radix UI, Lucide React
|
||||||
|
- **Charts**: Recharts untuk visualisasi data
|
||||||
- **Styling**: Tailwind CSS
|
- **Styling**: Tailwind CSS
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
@ -35,7 +39,7 @@ Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Pris
|
||||||
1. **Clone repository** (jika dari git)
|
1. **Clone repository** (jika dari git)
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd sipintar-app
|
cd SIPINTAR
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies**
|
2. **Install dependencies**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:v2.37.1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
command:
|
||||||
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
|
- '--storage.tsdb.path=/prometheus'
|
||||||
|
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||||
|
- '--web.console.templates=/etc/prometheus/consoles'
|
||||||
|
- '--web.enable-lifecycle'
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
configs:
|
||||||
|
- source: prometheus_config
|
||||||
|
target: /etc/prometheus/prometheus.yml
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
- sipintar-overlay
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana
|
||||||
|
ports:
|
||||||
|
- "4000:3000"
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
- sipintar-overlay
|
||||||
|
|
||||||
|
alertmanager:
|
||||||
|
image: prom/alertmanager
|
||||||
|
configs:
|
||||||
|
- source: alertmanager_config
|
||||||
|
target: /etc/alertmanager/config.yml
|
||||||
|
ports:
|
||||||
|
- "9093:9093"
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
- sipintar-overlay
|
||||||
|
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:2.8.2
|
||||||
|
ports:
|
||||||
|
- "3100:3100"
|
||||||
|
volumes:
|
||||||
|
- ./monitoring/loki/config.yml:/etc/loki/local-config.yml:ro
|
||||||
|
- loki-data:/loki
|
||||||
|
command:
|
||||||
|
- -config.file=/etc/loki/local-config.yml
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
- sipintar-overlay
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
|
||||||
|
promtail:
|
||||||
|
image: grafana/promtail:2.7.3
|
||||||
|
configs:
|
||||||
|
- source: promtail_config
|
||||||
|
target: /etc/promtail/config.yml
|
||||||
|
volumes:
|
||||||
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
- sipintar-overlay
|
||||||
|
|
||||||
|
node-exporter:
|
||||||
|
image: prom/node-exporter
|
||||||
|
ports:
|
||||||
|
- "9100:9100"
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
- sipintar-overlay
|
||||||
|
|
||||||
|
cadvisor:
|
||||||
|
image: gcr.io/cadvisor/cadvisor
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
networks:
|
||||||
|
- monitoring
|
||||||
|
- sipintar-overlay
|
||||||
|
|
||||||
|
configs:
|
||||||
|
prometheus_config:
|
||||||
|
file: ./monitoring/prometheus/prometheus.yml
|
||||||
|
alertmanager_config:
|
||||||
|
file: ./monitoring/alertmanager/config.yml
|
||||||
|
loki_config:
|
||||||
|
file: ./monitoring/loki/config.yml
|
||||||
|
promtail_config:
|
||||||
|
file: ./monitoring/promtail/config.yml
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sipintar-overlay:
|
||||||
|
driver: overlay
|
||||||
|
external: true
|
||||||
|
name: sipintar_stack_sipintar-overlay
|
||||||
|
monitoring:
|
||||||
|
driver: overlay
|
||||||
|
external: true
|
||||||
|
name: monitoring_sipintar-overlay
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
prometheus_data:
|
||||||
|
loki-data:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# docker stack deploy -c docker-compose-monitoring.yml monitoring
|
||||||
|
# docker stack rm monitoring
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: adelyao/sipintar-app:latest
|
||||||
|
working_dir: /app
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- sipintar-overlay
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 350M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.1'
|
||||||
|
memory: 100M
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
command: ["sh", "-c", "export DATABASE_URL=$$(cat /run/secrets/db_url) && npm run start"] #nanti tambahin biar prisma langsung di run
|
||||||
|
secrets:
|
||||||
|
- db_url
|
||||||
|
|
||||||
|
sipintar_mysql:
|
||||||
|
image: mysql:5.7
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: sipintar_school
|
||||||
|
MYSQL_USER: sipintar_user
|
||||||
|
MYSQL_PASSWORD_FILE: /run/secrets/db_password
|
||||||
|
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
- ./setup-database.sql:/docker-entrypoint-initdb.d/setup-database.sql
|
||||||
|
networks:
|
||||||
|
- sipintar-overlay
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p$$(cat /run/secrets/db_root_password)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
start_period: 40s
|
||||||
|
deploy:
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 350M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.1'
|
||||||
|
memory: 100M
|
||||||
|
secrets:
|
||||||
|
- db_password
|
||||||
|
- db_root_password
|
||||||
|
- db_url
|
||||||
|
|
||||||
|
|
||||||
|
scanner:
|
||||||
|
image: aquasec/trivy:latest
|
||||||
|
working_dir: /app
|
||||||
|
environment:
|
||||||
|
TRIVY_SEVERITY: "CRITICAL,HIGH"
|
||||||
|
TRIVY_IGNORE_UNFIXED: "true"
|
||||||
|
TRIVY_OUTPUT: "/app/trivy-report.json"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./trivy-results:/tmp/trivy-results
|
||||||
|
command: ["image", "-q", "--format", "json", "--severity", "CRITICAL,HIGH", "app", "--output", "/tmp/trivy-results/trivy-report.json"]
|
||||||
|
networks:
|
||||||
|
- sipintar-overlay
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 350M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.1'
|
||||||
|
memory: 100M
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
db_password:
|
||||||
|
file: ./secrets/db_password.txt
|
||||||
|
db_root_password:
|
||||||
|
file: ./secrets/db_root_password.txt
|
||||||
|
db_url:
|
||||||
|
file: ./secrets/db_url.txt
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sipintar-overlay:
|
||||||
|
driver: overlay
|
||||||
|
|
||||||
|
#docker-compose -f docker-compose-prod.yml --env-file .env.prod build
|
||||||
|
#docker-compose -f docker-compose-prod.yml --env-file .env.prod up -d
|
||||||
|
#docker-compose -f docker-compose-prod.yml --env-file .env.prod up (yg ada pilusnya)
|
||||||
|
#docker-compose -f docker-compose-prod.yml --env-file .env.prod up --build -d
|
||||||
|
#docker stack deploy -c docker-compose-prod.yml sipintar_stack
|
||||||
|
|
||||||
|
#docker-compose down
|
||||||
|
# docker system prune -f
|
||||||
|
# docker network prune -f
|
||||||
|
|
||||||
|
# docker stack rm sipintar_stack
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: sipintar-app
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /app
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: mysql://sipintar_user:sipintar_password123@db:3306/sipintar_school
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/.next
|
||||||
|
networks:
|
||||||
|
- sipintar-network
|
||||||
|
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:5.7
|
||||||
|
container_name: sipintar-mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root123
|
||||||
|
MYSQL_DATABASE: sipintar_school
|
||||||
|
MYSQL_USER: sipintar_user
|
||||||
|
MYSQL_PASSWORD: sipintar_password123
|
||||||
|
ports:
|
||||||
|
- '3307:3306'
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
- ./setup-database.sql:/docker-entrypoint-initdb.d/setup-database.sql
|
||||||
|
networks:
|
||||||
|
- sipintar-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
networks:
|
||||||
|
sipintar-network:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
global:
|
||||||
|
resolve_timeout: 5m
|
||||||
|
|
||||||
|
route:
|
||||||
|
receiver: 'default'
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
- name: 'default'
|
||||||
|
email_configs:
|
||||||
|
- to: 'youremail@example.com'
|
||||||
|
from: 'alert@example.com'
|
||||||
|
smarthost: 'smtp.gmail.com:587'
|
||||||
|
auth_username: 'alert@example.com'
|
||||||
|
auth_identity: 'alert@example.com'
|
||||||
|
auth_password: 'yourpassword'
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
auth_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_port: 3100
|
||||||
|
grpc_listen_port: 9096
|
||||||
|
|
||||||
|
ingester:
|
||||||
|
lifecycler:
|
||||||
|
address: 0.0.0.0
|
||||||
|
ring:
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
replication_factor: 1
|
||||||
|
chunk_idle_period: 5m
|
||||||
|
max_chunk_age: 1h
|
||||||
|
chunk_target_size: 1048576
|
||||||
|
chunk_retain_period: 30s
|
||||||
|
wal:
|
||||||
|
enabled: true
|
||||||
|
dir: /loki/wal
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: 2020-10-24
|
||||||
|
store: boltdb-shipper
|
||||||
|
object_store: filesystem
|
||||||
|
schema: v11
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
storage_config:
|
||||||
|
boltdb_shipper:
|
||||||
|
active_index_directory: /loki/index
|
||||||
|
cache_location: /loki/cache
|
||||||
|
shared_store: filesystem
|
||||||
|
filesystem:
|
||||||
|
directory: /loki/chunks
|
||||||
|
|
||||||
|
compactor:
|
||||||
|
working_directory: /loki/compactor
|
||||||
|
shared_store: filesystem
|
||||||
|
|
||||||
|
limits_config:
|
||||||
|
ingestion_rate_mb: 8
|
||||||
|
ingestion_burst_size_mb: 16
|
||||||
|
max_concurrent_tail_requests: 20
|
||||||
|
|
||||||
|
chunk_store_config:
|
||||||
|
max_look_back_period: 0s
|
||||||
|
|
||||||
|
table_manager:
|
||||||
|
retention_deletes_enabled: true
|
||||||
|
retention_period: 168h # 7 hari
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 15s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'sipintar-app'
|
||||||
|
metrics_path: '/api/metrics'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:3000']
|
||||||
|
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
|
||||||
|
- job_name: 'node-exporter'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['node-exporter:9100']
|
||||||
|
|
||||||
|
- job_name: 'cadvisor'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['cadvisor:8080']
|
||||||
|
|
||||||
|
- job_name: 'loki'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['loki:3100']
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
server:
|
||||||
|
http_listen_port: 9080
|
||||||
|
grpc_listen_port: 0
|
||||||
|
|
||||||
|
positions:
|
||||||
|
filename: /tmp/positions.yaml
|
||||||
|
|
||||||
|
clients:
|
||||||
|
- url: http://loki:3100/loki/api/v1/push
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: docker
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- localhost
|
||||||
|
labels:
|
||||||
|
job: varlogs
|
||||||
|
__path__: /var/lib/docker/containers/*/*.log
|
||||||
|
|
@ -41,8 +41,9 @@
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.4",
|
"eslint-config-next": "15.4.4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
|
@ -81,6 +82,30 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
||||||
|
|
@ -2630,6 +2655,34 @@
|
||||||
"tailwindcss": "4.1.11"
|
"tailwindcss": "4.1.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||||
|
|
@ -2783,17 +2836,17 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
||||||
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
|
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.38.0",
|
"@typescript-eslint/scope-manager": "8.39.1",
|
||||||
"@typescript-eslint/type-utils": "8.38.0",
|
"@typescript-eslint/type-utils": "8.39.1",
|
||||||
"@typescript-eslint/utils": "8.38.0",
|
"@typescript-eslint/utils": "8.39.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
|
|
@ -2807,9 +2860,9 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.38.0",
|
"@typescript-eslint/parser": "^8.39.1",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||||
|
|
@ -2823,16 +2876,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
|
||||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.38.0",
|
"@typescript-eslint/scope-manager": "8.39.1",
|
||||||
"@typescript-eslint/types": "8.38.0",
|
"@typescript-eslint/types": "8.39.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2844,18 +2897,18 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
|
||||||
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
|
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.38.0",
|
"@typescript-eslint/tsconfig-utils": "^8.39.1",
|
||||||
"@typescript-eslint/types": "^8.38.0",
|
"@typescript-eslint/types": "^8.39.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2866,18 +2919,18 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
|
||||||
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
|
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.38.0",
|
"@typescript-eslint/types": "8.39.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.38.0"
|
"@typescript-eslint/visitor-keys": "8.39.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -2888,9 +2941,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
|
||||||
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
|
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2901,19 +2954,19 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
|
||||||
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
|
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.38.0",
|
"@typescript-eslint/types": "8.39.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||||
"@typescript-eslint/utils": "8.38.0",
|
"@typescript-eslint/utils": "8.39.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2926,13 +2979,13 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
|
||||||
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
|
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2944,16 +2997,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
|
||||||
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
|
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.38.0",
|
"@typescript-eslint/project-service": "8.39.1",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.38.0",
|
"@typescript-eslint/tsconfig-utils": "8.39.1",
|
||||||
"@typescript-eslint/types": "8.38.0",
|
"@typescript-eslint/types": "8.39.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
|
|
@ -2969,7 +3022,7 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
|
|
@ -3029,16 +3082,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
|
||||||
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
|
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.38.0",
|
"@typescript-eslint/scope-manager": "8.39.1",
|
||||||
"@typescript-eslint/types": "8.38.0",
|
"@typescript-eslint/types": "8.39.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.38.0"
|
"@typescript-eslint/typescript-estree": "8.39.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3049,17 +3102,17 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.38.0",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
|
||||||
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
|
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.38.0",
|
"@typescript-eslint/types": "8.39.1",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3362,6 +3415,19 @@
|
||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
|
@ -3395,6 +3461,13 @@
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
|
@ -3887,6 +3960,13 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|
@ -4183,6 +4263,16 @@
|
||||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
|
|
@ -6435,6 +6525,13 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -8112,6 +8209,50 @@
|
||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.15.0",
|
"version": "3.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||||
|
|
@ -8243,9 +8384,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -8387,6 +8528,13 @@
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "37.3.6",
|
"version": "37.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
|
@ -8534,6 +8682,16 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.4",
|
"eslint-config-next": "15.4.4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -36,6 +36,13 @@ enum Role {
|
||||||
ADMIN
|
ADMIN
|
||||||
TEACHER
|
TEACHER
|
||||||
STUDENT
|
STUDENT
|
||||||
|
PARENT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Gender {
|
||||||
|
MALE
|
||||||
|
FEMALE
|
||||||
|
OTHER
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model untuk Siswa
|
// Model untuk Siswa
|
||||||
|
|
@ -44,6 +51,7 @@ model Student {
|
||||||
userId String @unique
|
userId String @unique
|
||||||
studentNumber String @unique
|
studentNumber String @unique
|
||||||
dateOfBirth DateTime
|
dateOfBirth DateTime
|
||||||
|
gender Gender @default(OTHER)
|
||||||
address String
|
address String
|
||||||
phone String?
|
phone String?
|
||||||
parentName String
|
parentName String
|
||||||
|
|
|
||||||
|
|
@ -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: 'STUDENT',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Create Teacher Users and Teacher profiles
|
// Create Teacher Users and Teacher profiles
|
||||||
const teacherPassword = await hashPassword('guru123')
|
const teacherPassword = await hashPassword('guru123')
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
|
|
@ -4,11 +4,11 @@ mysql -u root -p
|
||||||
# Buat database baru
|
# Buat database baru
|
||||||
CREATE DATABASE sipintar_school CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
CREATE DATABASE sipintar_school CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
# Buat user khusus (opsional tapi recommended)
|
# Buat user khusus
|
||||||
CREATE USER 'sipintar_user'@'localhost' IDENTIFIED BY 'sipintar_password123';
|
CREATE USER 'sipintar_user'@'%' IDENTIFIED BY 'sipintar_password123';
|
||||||
|
|
||||||
# Berikan akses ke database
|
# Berikan akses ke database
|
||||||
GRANT ALL PRIVILEGES ON sipintar_school.* TO 'sipintar_user'@'localhost';
|
GRANT ALL PRIVILEGES ON sipintar_school.* TO 'sipintar_user'@'%';
|
||||||
|
|
||||||
# Refresh privileges
|
# Refresh privileges
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,9 +23,9 @@ export async function GET(request: NextRequest) {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decoded.role !== 'ADMIN') {
|
if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: 'Access denied. Admin role required.' },
|
{ message: 'Access denied. Admin or Teacher role required.' },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -38,11 +38,49 @@ export async function GET(request: NextRequest) {
|
||||||
prisma.subject.count({ where: { isActive: true } }),
|
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<string, number>, curr) => {
|
||||||
|
const key = curr.grade
|
||||||
|
if (!acc[key]) {
|
||||||
|
acc[key] = 0
|
||||||
|
}
|
||||||
|
acc[key] += curr._count.id
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const classBySection = classDistribution.reduce((acc: Record<string, number>, 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({
|
return NextResponse.json({
|
||||||
totalStudents,
|
totalStudents,
|
||||||
totalTeachers,
|
totalTeachers,
|
||||||
totalClasses,
|
totalClasses,
|
||||||
totalSubjects,
|
totalSubjects,
|
||||||
|
studentsByGender: genderStats,
|
||||||
|
classByGrade,
|
||||||
|
classBySection,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Dashboard stats error:', error)
|
console.error('Dashboard stats error:', error)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
import { Registry, collectDefaultMetrics } from 'prom-client';
|
||||||
|
|
||||||
|
const register = new Registry();
|
||||||
|
collectDefaultMetrics({ register });
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return new Response(await register.metrics(), {
|
||||||
|
headers: { 'Content-Type': register.contentType },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,9 +27,18 @@ export default function LoginPage() {
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
// Store token or handle authentication
|
// Store token and user data
|
||||||
localStorage.setItem('token', data.token)
|
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 {
|
} else {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setError(data.message || 'Login failed')
|
setError(data.message || 'Login failed')
|
||||||
|
|
@ -71,7 +80,7 @@ export default function LoginPage() {
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
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"
|
placeholder="Masukkan email Anda"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -87,7 +96,7 @@ export default function LoginPage() {
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
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"
|
placeholder="Masukkan password Anda"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -137,9 +146,8 @@ export default function LoginPage() {
|
||||||
<div className="mt-8 p-4 bg-gray-50 rounded-md">
|
<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>
|
<h4 className="text-sm font-medium text-gray-900 mb-2">Demo Accounts:</h4>
|
||||||
<div className="text-xs text-gray-600 space-y-1">
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
<p><strong>Admin:</strong> admin@sipintar.com / admin123</p>
|
<p><strong>Admin (Guru):</strong> admin@sipintar.com / admin123</p>
|
||||||
<p><strong>Guru:</strong> guru@sipintar.com / guru123</p>
|
<p><strong>Orang Tua:</strong> parent@sipintar.com / parent123</p>
|
||||||
<p><strong>Siswa:</strong> siswa@sipintar.com / siswa123</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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<User | null>(null)
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalStudents: 0,
|
||||||
|
totalTeachers: 0,
|
||||||
|
totalClasses: 0,
|
||||||
|
totalSubjects: 0,
|
||||||
|
studentsByGender: {
|
||||||
|
male: 0,
|
||||||
|
female: 0,
|
||||||
|
other: 0
|
||||||
|
},
|
||||||
|
classByGrade: {} as Record<string, number>,
|
||||||
|
classBySection: {} as Record<string, number>,
|
||||||
|
})
|
||||||
|
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 <div className="flex items-center justify-center min-h-screen">Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
|
{/* Hover Sidebar */}
|
||||||
|
<div
|
||||||
|
className="hidden lg:flex lg:flex-shrink-0 group"
|
||||||
|
onMouseEnter={() => setSidebarHovered(true)}
|
||||||
|
onMouseLeave={() => setSidebarHovered(false)}
|
||||||
|
>
|
||||||
|
<div className={`flex flex-col transition-all duration-300 ease-in-out ${sidebarHovered ? 'w-64' : 'w-16'}`}>
|
||||||
|
<div className="flex flex-col h-0 flex-1 shadow-lg" style={{ backgroundColor: '#0D1320' }}>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center h-16 flex-shrink-0 px-4" style={{ backgroundColor: '#0D1320' }}>
|
||||||
|
<Image
|
||||||
|
src="/Logo Geometris dengan Topi Wisuda.png"
|
||||||
|
alt="SIPINTAR Logo"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className={`transition-all duration-300 ${sidebarHovered ? 'mr-3' : 'mx-auto'}`}
|
||||||
|
/>
|
||||||
|
<h1 className={`text-xl font-bold text-white transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
SIPINTAR
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="mt-5 flex-1 px-2 space-y-1">
|
||||||
|
<Link
|
||||||
|
href="/dashboard/admin"
|
||||||
|
className="bg-blue-800 bg-opacity-50 text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Dashboard"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6H8V5z" />
|
||||||
|
</svg>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/students"
|
||||||
|
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Siswa"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" 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>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Siswa
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/teachers"
|
||||||
|
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Guru"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" 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>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Guru
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/classes"
|
||||||
|
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Kelas"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" 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>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Kelas
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/subjects"
|
||||||
|
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Mata Pelajaran"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" 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>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Mata Pelajaran
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/attendance"
|
||||||
|
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Absensi"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Absensi
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/grades"
|
||||||
|
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Nilai"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Nilai
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/reports"
|
||||||
|
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||||
|
title="Laporan"
|
||||||
|
>
|
||||||
|
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
|
||||||
|
Laporan
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile sidebar */}
|
||||||
|
<div className={`lg:hidden fixed inset-0 flex z-40 ${sidebarOpen ? 'block' : 'hidden'}`}>
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)}></div>
|
||||||
|
<div className="relative flex-1 flex flex-col max-w-xs w-full" style={{ backgroundColor: '#0D1320' }}>
|
||||||
|
<div className="absolute top-0 right-0 -mr-12 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||||
|
>
|
||||||
|
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile navigation - same as desktop */}
|
||||||
|
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
|
||||||
|
<div className="flex items-center flex-shrink-0 px-4 py-4" style={{ backgroundColor: '#0D1320' }}>
|
||||||
|
<Image
|
||||||
|
src="/Logo Geometris dengan Topi Wisuda.png"
|
||||||
|
alt="SIPINTAR Logo"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="mr-3"
|
||||||
|
/>
|
||||||
|
<h1 className="text-xl font-bold text-white">
|
||||||
|
SIPINTAR
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<nav className="mt-5 px-2 space-y-1">
|
||||||
|
{/* Same navigation items as desktop */}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="flex flex-col w-0 flex-1 overflow-hidden">
|
||||||
|
{/* Top header */}
|
||||||
|
<div className="relative z-10 flex-shrink-0 flex h-16 bg-white shadow">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
className="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 lg:hidden"
|
||||||
|
>
|
||||||
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex-1 px-4 flex justify-between items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-2xl font-semibold text-gray-900">Dashboard Admin</h1>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex items-center md:ml-6">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-medium text-gray-900">{user.name}</p>
|
||||||
|
<p className="text-xs text-gray-500">{user.role}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content area */}
|
||||||
|
<main className="flex-1 relative overflow-y-auto focus:outline-none">
|
||||||
|
<div className="py-6">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
|
||||||
|
{/* Welcome Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
|
Selamat Datang, {user.name}!
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">Kelola sistem sekolah dengan mudah dan efisien</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards and Gender Statistics */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
{/* Gender Statistics Circle Chart */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Total Murid</h3>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="relative w-48 h-48 mb-4">
|
||||||
|
{/* SVG Circle Chart */}
|
||||||
|
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 200 200">
|
||||||
|
{/* Background circle */}
|
||||||
|
<circle
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="80"
|
||||||
|
fill="none"
|
||||||
|
stroke="#f3f4f6"
|
||||||
|
strokeWidth="20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Male segment */}
|
||||||
|
{stats.studentsByGender.male > 0 && (
|
||||||
|
<circle
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="80"
|
||||||
|
fill="none"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth="20"
|
||||||
|
strokeDasharray={`${(stats.studentsByGender.male / stats.totalStudents) * 502.65} 502.65`}
|
||||||
|
strokeDashoffset="0"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Female segment */}
|
||||||
|
{stats.studentsByGender.female > 0 && (
|
||||||
|
<circle
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="80"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ec4899"
|
||||||
|
strokeWidth="20"
|
||||||
|
strokeDasharray={`${(stats.studentsByGender.female / stats.totalStudents) * 502.65} 502.65`}
|
||||||
|
strokeDashoffset={`-${(stats.studentsByGender.male / stats.totalStudents) * 502.65}`}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Center number */}
|
||||||
|
<text x="100" y="108" textAnchor="middle" style={{ fill: '#111827', fontSize: '28px', fontWeight: '700', fontStyle: 'normal' }} transform="rotate(90 100 100)">
|
||||||
|
{stats.totalStudents}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="space-y-2 w-full">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-3 h-3 bg-blue-500 rounded-full mr-2"></div>
|
||||||
|
<span className="text-sm text-gray-700">Laki-laki</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-sm font-bold text-gray-900">{stats.studentsByGender.male}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-1">
|
||||||
|
({stats.totalStudents > 0 ? Math.round((stats.studentsByGender.male / stats.totalStudents) * 100) : 0}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-3 h-3 bg-pink-500 rounded-full mr-2"></div>
|
||||||
|
<span className="text-sm text-gray-700">Perempuan</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="text-sm font-bold text-gray-900">{stats.studentsByGender.female}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-1">
|
||||||
|
({stats.totalStudents > 0 ? Math.round((stats.studentsByGender.female / stats.totalStudents) * 100) : 0}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-white" 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>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Total Siswa</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalStudents}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-white" 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>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Total Guru</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalTeachers}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-white" 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>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Total Kelas</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalClasses}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-8 h-8 bg-orange-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-white" 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>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Total Mata Pelajaran</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{stats.totalSubjects}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Aksi Cepat</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Link
|
||||||
|
href="/dashboard/students"
|
||||||
|
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-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>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">Kelola Siswa</p>
|
||||||
|
<p className="text-sm text-gray-500">Tambah, edit, atau lihat data siswa</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/teachers"
|
||||||
|
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-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="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>
|
||||||
|
<p className="font-medium text-gray-900">Kelola Guru</p>
|
||||||
|
<p className="text-sm text-gray-500">Tambah, edit, atau lihat data guru</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/dashboard/classes"
|
||||||
|
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-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="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>
|
||||||
|
<p className="font-medium text-gray-900">Kelola Kelas</p>
|
||||||
|
<p className="text-sm text-gray-500">Atur kelas dan mata pelajaran</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Class Distribution Bar Chart */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-6">Distribusi Kelas</h3>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Stacked Bar Chart by Grade with Sections */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md font-medium text-gray-700 mb-4">Distribusi Kelas Berdasarkan Tingkat dan Jurusan</h4>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="flex flex-wrap gap-4 mb-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-4 h-4 bg-blue-500 rounded mr-2"></div>
|
||||||
|
<span className="text-sm text-gray-700">IPA</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-4 h-4 bg-green-500 rounded mr-2"></div>
|
||||||
|
<span className="text-sm text-gray-700">IPS</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-4 h-4 bg-orange-500 rounded mr-2"></div>
|
||||||
|
<span className="text-sm text-gray-700">BAHASA</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-4 h-4 bg-purple-500 rounded mr-2"></div>
|
||||||
|
<span className="text-sm text-gray-700">Lainnya</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stacked Bars */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{(() => {
|
||||||
|
// Group classes by grade and section
|
||||||
|
const gradeData: Record<string, Record<string, number>> = {}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div key={grade} className="flex items-center">
|
||||||
|
{/* Grade Label */}
|
||||||
|
<div className="w-20 text-sm font-medium text-gray-700 text-right mr-4">
|
||||||
|
Kelas {grade}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stacked Bar Container */}
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<div
|
||||||
|
className="flex h-8 bg-gray-100 rounded overflow-hidden"
|
||||||
|
style={{ width: `${Math.max(totalPercentage, 20)}%` }}
|
||||||
|
>
|
||||||
|
{/* IPA Segment */}
|
||||||
|
{sections.IPA > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 flex items-center justify-center text-white text-xs font-medium"
|
||||||
|
style={{ width: `${(sections.IPA / total) * 100}%` }}
|
||||||
|
>
|
||||||
|
{sections.IPA > 0 && total > 5 && sections.IPA}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* IPS Segment */}
|
||||||
|
{sections.IPS > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-green-500 flex items-center justify-center text-white text-xs font-medium"
|
||||||
|
style={{ width: `${(sections.IPS / total) * 100}%` }}
|
||||||
|
>
|
||||||
|
{sections.IPS > 0 && total > 5 && sections.IPS}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* BAHASA Segment */}
|
||||||
|
{sections.BAHASA > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-orange-500 flex items-center justify-center text-white text-xs font-medium"
|
||||||
|
style={{ width: `${(sections.BAHASA / total) * 100}%` }}
|
||||||
|
>
|
||||||
|
{sections.BAHASA > 0 && total > 3 && sections.BAHASA}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* OTHER Segment */}
|
||||||
|
{sections.OTHER > 0 && (
|
||||||
|
<div
|
||||||
|
className="bg-purple-500 flex items-center justify-center text-white text-xs font-medium"
|
||||||
|
style={{ width: `${(sections.OTHER / total) * 100}%` }}
|
||||||
|
>
|
||||||
|
{sections.OTHER > 0 && total > 3 && sections.OTHER}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Total Count */}
|
||||||
|
<div className="w-12 text-right text-sm text-gray-600 ml-4">
|
||||||
|
{total}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Object.keys(stats.classByGrade).length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<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="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>
|
||||||
|
<p className="mt-2 text-gray-500 text-sm">Belum ada data kelas untuk ditampilkan</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Aktivitas Terbaru</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900">Siswa baru ditambahkan</p>
|
||||||
|
<p className="text-xs text-gray-500">2 jam yang lalu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900">Laporan absensi bulan ini</p>
|
||||||
|
<p className="text-xs text-gray-500">5 jam yang lalu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-3"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900">Nilai ujian diperbarui</p>
|
||||||
|
<p className="text-xs text-gray-500">1 hari yang lalu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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<Class[]>([])
|
||||||
|
const [teachers, setTeachers] = useState<Teacher[]>([])
|
||||||
|
const [subjects, setSubjects] = useState<Subject[]>([])
|
||||||
|
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<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-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 Kelas</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/dashboard/admin')}
|
||||||
|
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 Kelas</h2>
|
||||||
|
<p className="text-gray-600">Kelola kelas dan mata pelajaran</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddForm(true)}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
||||||
|
>
|
||||||
|
+ Tambah Kelas
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Classes Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{classes.length === 0 ? (
|
||||||
|
<div className="col-span-full">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<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="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>
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900">Belum ada kelas</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Mulai dengan menambahkan kelas 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-purple-600 hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
+ Tambah Kelas Pertama
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
classes.map((classItem) => (
|
||||||
|
<div key={classItem.id} className="bg-white rounded-lg shadow-sm border p-6 hover:shadow-md transition-shadow">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
{classItem.name}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium">Tingkat:</span>
|
||||||
|
<span className="ml-2">{classItem.grade} - {classItem.section}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium">Ruang:</span>
|
||||||
|
<span className="ml-2">{classItem.room}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium">Wali Kelas:</span>
|
||||||
|
<span className="ml-2">{classItem.teacher.user.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium">Mata Pelajaran:</span>
|
||||||
|
<span className="ml-2">{classItem.subject.name} ({classItem.subject.code})</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium">Siswa:</span>
|
||||||
|
<span className="ml-2">
|
||||||
|
{classItem._count.students} / {classItem.maxStudents}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${classItem._count.students >= 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'
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex space-x-3">
|
||||||
|
<button className="text-purple-600 hover:text-purple-900 text-sm font-medium">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button className="text-red-600 hover:text-red-900 text-sm font-medium">
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Class 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-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-6">Tambah Kelas Baru</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nama Kelas *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
placeholder="Contoh: X-IPA-1"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tingkat/Grade *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="grade"
|
||||||
|
value={formData.grade}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<option value="">Pilih Tingkat</option>
|
||||||
|
<option value="X">X (Sepuluh)</option>
|
||||||
|
<option value="XI">XI (Sebelas)</option>
|
||||||
|
<option value="XII">XII (Dua Belas)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Jurusan *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="section"
|
||||||
|
value={formData.section}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<option value="">Pilih Jurusan</option>
|
||||||
|
<option value="IPA">IPA</option>
|
||||||
|
<option value="IPS">IPS</option>
|
||||||
|
<option value="BAHASA">BAHASA</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Ruang Kelas *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="room"
|
||||||
|
value={formData.room}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
placeholder="Contoh: R101"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Maksimal Siswa *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="maxStudents"
|
||||||
|
value={formData.maxStudents}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
max="50"
|
||||||
|
placeholder="30"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Wali Kelas *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="teacherId"
|
||||||
|
value={formData.teacherId}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<option value="">Pilih Wali Kelas</option>
|
||||||
|
{teachers.map((teacher) => (
|
||||||
|
<option key={teacher.id} value={teacher.id}>
|
||||||
|
{teacher.user.name} ({teacher.teacherNumber})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Mata Pelajaran *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="subjectId"
|
||||||
|
value={formData.subjectId}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<option value="">Pilih Mata Pelajaran</option>
|
||||||
|
{subjects.map((subject) => (
|
||||||
|
<option key={subject.id} value={subject.id}>
|
||||||
|
{subject.name} ({subject.code})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 pt-6 border-t">
|
||||||
|
<button
|
||||||
|
type="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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Kelas'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "dashboard",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
|
|
@ -3,426 +3,556 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from 'recharts'
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
Cell,
|
||||||
|
} from 'recharts'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
email: string
|
email: string
|
||||||
name: string
|
name: string
|
||||||
role: string
|
role: string
|
||||||
profileImage?: string
|
profileImage?: string
|
||||||
studentId?: string
|
studentId?: string
|
||||||
teacherId?: string
|
teacherId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardStats {
|
interface DashboardStats {
|
||||||
totalStudents: number
|
totalStudents: number
|
||||||
totalTeachers: number
|
totalTeachers: number
|
||||||
totalClasses: number
|
totalClasses: number
|
||||||
totalSubjects: number
|
totalSubjects: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChartData {
|
interface ChartData {
|
||||||
enrollmentData: { name: string; students: number }[]
|
enrollmentData: { name: string; students: number }[]
|
||||||
attendanceData: { month: string; present: number; absent: number; late: number }[]
|
attendanceData: {
|
||||||
gradeData: { grade: string; count: number }[]
|
month: string
|
||||||
subjectData: { name: string; teachers: number }[]
|
present: number
|
||||||
|
absent: number
|
||||||
|
late: number
|
||||||
|
}[]
|
||||||
|
gradeData: { grade: string; count: number }[]
|
||||||
|
subjectData: { name: string; teachers: number }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const [user, setUser] = useState<User | null>(null)
|
const [user, setUser] = useState<User | null>(null)
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||||
const [chartData, setChartData] = useState<ChartData | null>(null)
|
const [chartData, setChartData] = useState<ChartData | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (!token) {
|
if (!token) {
|
||||||
router.push('/auth/login')
|
router.push('/auth/login')
|
||||||
return
|
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 = () => {
|
// Parse JWT to get user info (in production, verify with server)
|
||||||
localStorage.removeItem('token')
|
try {
|
||||||
router.push('/auth/login')
|
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])
|
||||||
|
|
||||||
if (isLoading) {
|
const fetchDashboardData = async () => {
|
||||||
return (
|
try {
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
const token = localStorage.getItem('token')
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
||||||
</div>
|
// 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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className='min-h-screen flex items-center justify-center'>
|
||||||
{/* Navigation */}
|
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600'></div>
|
||||||
<nav className="bg-white shadow-sm border-b">
|
</div>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistik kehadiran Jarwo untuk dashboard admin
|
||||||
|
const jarwoAttendanceWeek = [
|
||||||
|
{ day: 'Senin', hadir: true },
|
||||||
|
{ day: 'Selasa', hadir: false },
|
||||||
|
{ day: 'Rabu', hadir: true },
|
||||||
|
{ day: 'Kamis', hadir: false },
|
||||||
|
{ day: 'Jumat', hadir: true },
|
||||||
|
]
|
||||||
|
const jarwoHadirCount = jarwoAttendanceWeek.filter((a) => a.hadir).length
|
||||||
|
|
||||||
|
if (user?.role === 'ADMIN' && stats) {
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen bg-gray-50'>
|
||||||
|
<nav className='bg-white shadow-sm border-b'>
|
||||||
|
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
|
||||||
|
<div className='flex justify-between h-16'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<h1 className='text-xl font-bold text-gray-900'>SIPINTAR</h1>
|
||||||
|
<span className='ml-2 text-sm text-gray-500'>
|
||||||
|
Dashboard Admin
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center space-x-4'>
|
||||||
|
<span className='text-sm text-gray-700'>
|
||||||
|
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className='text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium'
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8'>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'>
|
||||||
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div className='p-3 rounded-full bg-blue-100'>
|
||||||
|
<svg
|
||||||
|
className='w-6 h-6 text-blue-600'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className='ml-4'>
|
||||||
|
<p className='text-sm font-medium text-gray-500'>
|
||||||
|
Total Siswa
|
||||||
|
</p>
|
||||||
|
<p className='text-2xl font-semibold text-gray-900'>
|
||||||
|
{stats.totalStudents}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div className='p-3 rounded-full bg-green-100'>
|
||||||
|
<svg
|
||||||
|
className='w-6 h-6 text-green-600'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className='ml-4'>
|
||||||
|
<p className='text-sm font-medium text-gray-500'>
|
||||||
|
Total Guru
|
||||||
|
</p>
|
||||||
|
<p className='text-2xl font-semibold text-gray-900'>
|
||||||
|
{stats.totalTeachers}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div className='p-3 rounded-full bg-purple-100'>
|
||||||
|
<svg
|
||||||
|
className='w-6 h-6 text-purple-600'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className='ml-4'>
|
||||||
|
<p className='text-sm font-medium text-gray-500'>
|
||||||
|
Total Kelas
|
||||||
|
</p>
|
||||||
|
<p className='text-2xl font-semibold text-gray-900'>
|
||||||
|
{stats.totalClasses}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div className='p-3 rounded-full bg-yellow-100'>
|
||||||
|
<svg
|
||||||
|
className='w-6 h-6 text-yellow-600'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className='ml-4'>
|
||||||
|
<p className='text-sm font-medium text-gray-500'>
|
||||||
|
Mata Pelajaran
|
||||||
|
</p>
|
||||||
|
<p className='text-2xl font-semibold text-gray-900'>
|
||||||
|
{stats.totalSubjects}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Statistik Kehadiran Jarwo */}
|
||||||
|
<div className='mb-8'>
|
||||||
|
<h3 className='text-2xl font-bold text-blue-700 mb-6 flex items-center gap-2'>
|
||||||
|
<svg
|
||||||
|
className='w-7 h-7 text-blue-500'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Statistik Kehadiran Siswa:{' '}
|
||||||
|
<span className='text-blue-900'>Jarwo</span>
|
||||||
|
</h3>
|
||||||
|
<div className='bg-gradient-to-br from-blue-50 to-green-50 rounded-xl shadow-lg border border-blue-100 p-8 mb-6'>
|
||||||
|
<div className='flex items-center justify-between mb-4'>
|
||||||
|
<div>
|
||||||
|
<p className='text-lg font-semibold text-gray-900'>
|
||||||
|
Kehadiran Minggu Ini
|
||||||
|
</p>
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
Periode: <span className='font-medium'>Senin - Jumat</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='px-3 py-1 rounded-full bg-green-100 text-green-700 font-bold text-sm flex items-center gap-1'>
|
||||||
|
<svg
|
||||||
|
className='w-4 h-4'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M5 13l4 4L19 7'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{jarwoHadirCount} Hadir
|
||||||
|
</span>
|
||||||
|
<span className='px-3 py-1 rounded-full bg-red-100 text-red-700 font-bold text-sm flex items-center gap-1'>
|
||||||
|
<svg
|
||||||
|
className='w-4 h-4'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M6 18L18 6M6 6l12 12'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{5 - jarwoHadirCount} Tidak Hadir
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 mb-4'>
|
||||||
|
{jarwoAttendanceWeek.map((a, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`flex flex-col items-center py-3 rounded-lg border ${
|
||||||
|
a.hadir
|
||||||
|
? 'bg-green-50 border-green-200'
|
||||||
|
: 'bg-red-50 border-red-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className='text-gray-700 font-medium mb-1'>
|
||||||
|
{a.day}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
a.hadir
|
||||||
|
? 'text-green-600 font-bold'
|
||||||
|
: 'text-red-500 font-bold'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{a.hadir ? (
|
||||||
|
<span className='flex items-center gap-1'>
|
||||||
|
<svg
|
||||||
|
className='w-4 h-4'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M5 13l4 4L19 7'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Hadir
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className='flex items-center gap-1'>
|
||||||
|
<svg
|
||||||
|
className='w-4 h-4'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M6 18L18 6M6 6l12 12'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Tidak Hadir
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='mt-4'>
|
||||||
|
<p className='text-sm text-gray-600 mb-2'>
|
||||||
|
Progress Kehadiran Mingguan
|
||||||
|
</p>
|
||||||
|
<div className='w-full h-5 bg-gray-200 rounded-full overflow-hidden'>
|
||||||
|
<div
|
||||||
|
className='h-5 bg-gradient-to-r from-green-400 to-blue-400 rounded-full transition-all duration-500'
|
||||||
|
style={{ width: `${(jarwoHadirCount / 7) * 100}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p className='text-xs text-gray-500 mt-2'>
|
||||||
|
{jarwoHadirCount} dari 7 hari hadir
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white rounded-xl shadow-lg border border-blue-100 p-8'>
|
||||||
|
<h4 className='text-lg font-semibold text-blue-700 mb-4 flex items-center gap-2'>
|
||||||
|
<svg
|
||||||
|
className='w-5 h-5 text-blue-400'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
strokeWidth={2}
|
||||||
|
d='M9 17v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Grafik Kehadiran Mingguan
|
||||||
|
</h4>
|
||||||
|
<ResponsiveContainer width='100%' height={180}>
|
||||||
|
<BarChart
|
||||||
|
data={jarwoAttendanceWeek.map((a) => ({
|
||||||
|
day: a.day,
|
||||||
|
hadir: a.hadir ? 1 : 0,
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray='3 3' />
|
||||||
|
<XAxis dataKey='day' />
|
||||||
|
<YAxis allowDecimals={false} domain={[0, 1]} />
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value) =>
|
||||||
|
value === 1 ? 'Hadir' : 'Tidak Hadir'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Bar dataKey='hadir' fill='#38bdf8' radius={[8, 8, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom dashboard for student named Jarwo
|
||||||
|
if (user?.role === 'STUDENT' && user?.name?.toLowerCase() === 'jarwo') {
|
||||||
|
// Simulated attendance data for Jarwo
|
||||||
|
const attendanceWeek = [
|
||||||
|
{ day: 'Senin', hadir: true },
|
||||||
|
{ day: 'Selasa', hadir: false },
|
||||||
|
{ day: 'Rabu', hadir: true },
|
||||||
|
{ day: 'Kamis', hadir: false },
|
||||||
|
{ day: 'Jumat', hadir: true },
|
||||||
|
]
|
||||||
|
const hadirCount = attendanceWeek.filter((a) => a.hadir).length
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen bg-gray-50'>
|
||||||
|
<nav className='bg-white shadow-sm border-b'>
|
||||||
|
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
|
||||||
|
<div className='flex justify-between h-16'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<h1 className='text-xl font-bold text-gray-900'>SIPINTAR</h1>
|
||||||
|
<span className='ml-2 text-sm text-gray-500'>
|
||||||
|
Dashboard Siswa
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center space-x-4'>
|
||||||
|
<span className='text-sm text-gray-700'>
|
||||||
|
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className='text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium'
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className='max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8'>
|
||||||
|
<div className='mb-8'>
|
||||||
|
<h2 className='text-2xl font-bold text-gray-900'>
|
||||||
|
Statistik Kehadiran Mingguan
|
||||||
|
</h2>
|
||||||
|
<p className='text-gray-600 mb-2'>
|
||||||
|
Siswa: <strong>Jarwo</strong>
|
||||||
|
</p>
|
||||||
|
<div className='bg-white rounded-lg shadow-sm border p-6 mb-6'>
|
||||||
|
<p className='text-lg font-semibold text-gray-900 mb-2'>
|
||||||
|
Kehadiran Minggu Ini
|
||||||
|
</p>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
{attendanceWeek.map((a, idx) => (
|
||||||
|
<div key={idx} className='flex justify-between items-center'>
|
||||||
|
<span className='text-gray-700'>{a.day}</span>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
a.hadir
|
||||||
|
? 'text-green-600 font-bold'
|
||||||
|
: 'text-red-500 font-bold'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{a.hadir ? 'Hadir' : 'Tidak Hadir'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='mt-4'>
|
||||||
|
<p className='text-sm text-gray-600'>
|
||||||
|
Total hadir:{' '}
|
||||||
|
<span className='font-bold text-green-600'>{hadirCount}</span>{' '}
|
||||||
|
dari 7 hari
|
||||||
|
</p>
|
||||||
|
<div className='w-full h-4 bg-gray-200 rounded-full mt-2'>
|
||||||
|
<div
|
||||||
|
className='h-4 bg-green-500 rounded-full'
|
||||||
|
style={{ width: `${(hadirCount / 7) * 100}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white rounded-lg shadow-sm border p-6'>
|
||||||
|
<h3 className='text-lg font-semibold text-gray-900 mb-2'>
|
||||||
|
Grafik Kehadiran
|
||||||
|
</h3>
|
||||||
|
<ResponsiveContainer width='100%' height={220}>
|
||||||
|
<BarChart
|
||||||
|
data={attendanceWeek.map((a) => ({
|
||||||
|
day: a.day,
|
||||||
|
hadir: a.hadir ? 1 : 0,
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray='3 3' />
|
||||||
|
<XAxis dataKey='day' />
|
||||||
|
<YAxis allowDecimals={false} domain={[0, 1]} />
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value) =>
|
||||||
|
value === 1 ? 'Hadir' : 'Tidak Hadir'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Bar dataKey='hadir' fill='#10B981' />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// ...existing code...
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<User | null>(null)
|
||||||
|
const [children] = useState<Child[]>([
|
||||||
|
{
|
||||||
|
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 <div className="flex items-center justify-center min-h-screen">Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Header */}
|
||||||
|
<header 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 items-center h-16">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Image
|
||||||
|
src="/Logo Geometris dengan Topi Wisuda.png"
|
||||||
|
alt="SIPINTAR Logo"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<h1 className="text-xl font-bold text-blue-700">SIPINTAR</h1>
|
||||||
|
<span className="text-gray-500">|</span>
|
||||||
|
<span className="text-gray-600">Dashboard Orang Tua</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-medium text-gray-900">{user.name}</p>
|
||||||
|
<p className="text-xs text-gray-500">Orang Tua</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main 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 mb-2">
|
||||||
|
Selamat Datang, {user.name}!
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">Monitor perkembangan akademik anak Anda</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Children Cards */}
|
||||||
|
<div className="grid gap-6 mb-8">
|
||||||
|
{children.map((child) => (
|
||||||
|
<div key={child.id} className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900">{child.name}</h3>
|
||||||
|
<p className="text-gray-600">NIS: {child.studentNumber} | Kelas: {child.class}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-8 h-8 text-blue-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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-green-700">Kehadiran</p>
|
||||||
|
<p className="text-xl font-bold text-green-900">{child.attendance}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<svg className="w-5 h-5 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>
|
||||||
|
<p className="text-sm font-medium text-blue-700">Nilai Terakhir</p>
|
||||||
|
<p className="text-xl font-bold text-blue-900">{child.lastGrade}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-purple-50 p-4 rounded-lg border border-purple-200">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<svg className="w-5 h-5 text-purple-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>
|
||||||
|
<p className="text-sm font-medium text-purple-700">Mata Pelajaran</p>
|
||||||
|
<p className="text-xl font-bold text-purple-900">12</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions for this child */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<button className="flex items-center justify-center p-3 border border-blue-200 rounded-lg hover:bg-blue-50 transition-colors">
|
||||||
|
<svg className="w-5 h-5 text-blue-600 mr-2" 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>
|
||||||
|
<span className="text-sm font-medium text-blue-700">Lihat Nilai</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center justify-center p-3 border border-green-200 rounded-lg hover:bg-green-50 transition-colors">
|
||||||
|
<svg className="w-5 h-5 text-green-600 mr-2" 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>
|
||||||
|
<span className="text-sm font-medium text-green-700">Absensi</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center justify-center p-3 border border-purple-200 rounded-lg hover:bg-purple-50 transition-colors">
|
||||||
|
<svg className="w-5 h-5 text-purple-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3a4 4 0 118 0v4m-4 6v6m0 0v3m0-3h6m-6 0H6" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium text-purple-700">Jadwal</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Updates */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Pemberitahuan Terbaru</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-blue-900">Nilai Ujian Matematika</p>
|
||||||
|
<p className="text-xs text-blue-700 mt-1">Ahmad Pratama mendapat nilai A untuk ujian matematika</p>
|
||||||
|
<p className="text-xs text-blue-600 mt-1">2 jam yang lalu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start p-4 bg-green-50 rounded-lg border border-green-200">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-green-900">Kehadiran Sempurna</p>
|
||||||
|
<p className="text-xs text-green-700 mt-1">Ahmad Pratama hadir tepat waktu hari ini</p>
|
||||||
|
<p className="text-xs text-green-600 mt-1">5 jam yang lalu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start p-4 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||||
|
<div className="w-2 h-2 bg-yellow-500 rounded-full mt-2 mr-3"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-yellow-900">Pengumuman Sekolah</p>
|
||||||
|
<p className="text-xs text-yellow-700 mt-1">Rapat orang tua akan dilaksanakan minggu depan</p>
|
||||||
|
<p className="text-xs text-yellow-600 mt-1">1 hari yang lalu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact School */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Hubungi Sekolah</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center p-4 border border-gray-200 rounded-lg">
|
||||||
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-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="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">Telepon</p>
|
||||||
|
<p className="text-sm text-gray-600">+62 21 1234-5678</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center p-4 border border-gray-200 rounded-lg">
|
||||||
|
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">Email</p>
|
||||||
|
<p className="text-sm text-gray-600">info@sipintar.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,19 @@ export default function StudentsPage() {
|
||||||
const [students, setStudents] = useState<Student[]>([])
|
const [students, setStudents] = useState<Student[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [showAddForm, setShowAddForm] = useState(false)
|
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()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -53,6 +66,61 @@ export default function StudentsPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
|
@ -73,7 +141,7 @@ export default function StudentsPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/dashboard')}
|
onClick={() => router.push('/dashboard/admin')}
|
||||||
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||||
>
|
>
|
||||||
← Kembali ke Dashboard
|
← Kembali ke Dashboard
|
||||||
|
|
@ -185,18 +253,179 @@ export default function StudentsPage() {
|
||||||
{/* Add Student Modal */}
|
{/* Add Student Modal */}
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<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">
|
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Tambah Siswa Baru</h3>
|
<div className="p-6">
|
||||||
<p className="text-sm text-gray-600 mb-4">
|
<h3 className="text-lg font-medium text-gray-900 mb-6">Tambah Siswa Baru</h3>
|
||||||
Fitur ini akan segera tersedia. Gunakan API endpoint /api/students untuk menambah siswa.
|
|
||||||
</p>
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<button
|
{/* Data Siswa */}
|
||||||
onClick={() => setShowAddForm(false)}
|
<div className="space-y-4">
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
|
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Siswa</h4>
|
||||||
>
|
|
||||||
Tutup
|
<div>
|
||||||
</button>
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nama Lengkap *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Password *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nomor Induk Siswa (NIS) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="studentNumber"
|
||||||
|
value={formData.studentNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tanggal Lahir *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="dateOfBirth"
|
||||||
|
value={formData.dateOfBirth}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
No. Telefon Siswa
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data Orang Tua */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Orang Tua</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nama Orang Tua/Wali *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="parentName"
|
||||||
|
value={formData.parentName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
No. Telefon Orang Tua *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="parentPhone"
|
||||||
|
value={formData.parentPhone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Kontak Darurat
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="emergencyContact"
|
||||||
|
value={formData.emergencyContact}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Alamat *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="address"
|
||||||
|
value={formData.address}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 pt-6 border-t">
|
||||||
|
<button
|
||||||
|
type="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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Siswa'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,432 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
interface Teacher {
|
||||||
|
id: string
|
||||||
|
user: {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
teacherNumber: string
|
||||||
|
specialization: string
|
||||||
|
qualification: string
|
||||||
|
experience: number
|
||||||
|
phone?: string
|
||||||
|
address?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TeachersPage() {
|
||||||
|
const [teachers, setTeachers] = useState<Teacher[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false)
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
teacherNumber: '',
|
||||||
|
specialization: '',
|
||||||
|
qualification: '',
|
||||||
|
experience: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
})
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (!token) {
|
||||||
|
router.push('/auth/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchTeachers()
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
const fetchTeachers = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await fetch('/api/teachers', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
setTeachers(data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch teachers:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
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/teachers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...formData,
|
||||||
|
experience: parseInt(formData.experience) || 0
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Reset form and close modal
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
teacherNumber: '',
|
||||||
|
specialization: '',
|
||||||
|
qualification: '',
|
||||||
|
experience: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
})
|
||||||
|
setShowAddForm(false)
|
||||||
|
|
||||||
|
// Refresh teachers list
|
||||||
|
await fetchTeachers()
|
||||||
|
|
||||||
|
alert('Guru berhasil ditambahkan!')
|
||||||
|
} else {
|
||||||
|
const error = await response.json()
|
||||||
|
alert(error.message || 'Gagal menambahkan guru')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding teacher:', error)
|
||||||
|
alert('Terjadi kesalahan saat menambahkan guru')
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(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 Guru</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/dashboard/admin')}
|
||||||
|
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 Guru</h2>
|
||||||
|
<p className="text-gray-600">Kelola data guru dan informasi akademik</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddForm(true)}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
||||||
|
>
|
||||||
|
+ Tambah Guru
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Teachers 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 Guru</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{teachers.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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900">Belum ada guru</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Mulai dengan menambahkan guru 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-green-600 hover:bg-green-700"
|
||||||
|
>
|
||||||
|
+ Tambah Guru 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">
|
||||||
|
Guru
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
NIP
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Spesialisasi
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Pengalaman
|
||||||
|
</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">
|
||||||
|
Aksi
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{teachers.map((teacher) => (
|
||||||
|
<tr key={teacher.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-900">
|
||||||
|
{teacher.user.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{teacher.user.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{teacher.teacherNumber}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="text-sm text-gray-900">{teacher.specialization}</div>
|
||||||
|
<div className="text-sm text-gray-500">{teacher.qualification}</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{teacher.experience} tahun
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
<div>{teacher.phone || 'Tidak ada'}</div>
|
||||||
|
<div className="text-xs">{teacher.address || 'Tidak ada'}</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<button className="text-green-600 hover:text-green-900 mr-3">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button className="text-red-600 hover:text-red-900">
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Teacher 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-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-6">Tambah Guru Baru</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* Data Pribadi */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Pribadi</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nama Lengkap *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Password *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nomor Induk Pegawai (NIP) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="teacherNumber"
|
||||||
|
value={formData.teacherNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
No. Telefon
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data Akademik */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Akademik</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Spesialisasi/Bidang Studi *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="specialization"
|
||||||
|
value={formData.specialization}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
placeholder="Contoh: Matematika, Bahasa Indonesia"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Kualifikasi Pendidikan *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="qualification"
|
||||||
|
value={formData.qualification}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
placeholder="Contoh: S1 Pendidikan Matematika"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Pengalaman Mengajar (tahun) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="experience"
|
||||||
|
value={formData.experience}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Alamat
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="address"
|
||||||
|
value={formData.address}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 pt-6 border-t">
|
||||||
|
<button
|
||||||
|
type="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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Guru'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
141
src/app/page.tsx
141
src/app/page.tsx
|
|
@ -1,142 +1,5 @@
|
||||||
import Link from "next/link";
|
import LandingPage from '@/components/LandingPage'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return <LandingPage />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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"
|
|
||||||
>
|
|
||||||
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 */}
|
|
||||||
<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,254 @@
|
||||||
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="absolute top-0 w-full z-10 px-6 py-4">
|
||||||
|
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Image
|
||||||
|
src="/Logo Geometris dengan Topi Wisuda.png"
|
||||||
|
alt="SIPINTAR Logo"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="drop-shadow-md"
|
||||||
|
/>
|
||||||
|
<span className="text-2xl font-bold text-blue-700">SIPINTAR</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-8">
|
||||||
|
<div className="hidden md:flex space-x-8">
|
||||||
|
<a href="#features" className="text-gray-700 hover:text-blue-700 transition-colors font-medium">Fitur</a>
|
||||||
|
<a href="#about" className="text-gray-700 hover:text-blue-700 transition-colors font-medium">Tentang</a>
|
||||||
|
<a href="#contact" className="text-gray-700 hover:text-blue-700 transition-colors font-medium">Kontak</a>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="bg-blue-700 text-white px-6 py-2 rounded-lg hover:bg-blue-800 transition-colors shadow-lg"
|
||||||
|
>
|
||||||
|
Masuk
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="pt-20 pb-16 px-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="text-center py-20">
|
||||||
|
<div className="mb-8 flex justify-center">
|
||||||
|
<Image
|
||||||
|
src="/Logo Geometris dengan Topi Wisuda.png"
|
||||||
|
alt="SIPINTAR Logo"
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
className="drop-shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-5xl md:text-7xl font-bold text-gray-800 mb-6">
|
||||||
|
<span className="text-blue-700">SI</span>PINTAR
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-700 mb-8 max-w-3xl mx-auto">
|
||||||
|
Sistem Pemantauan Interaktif dan Pintar
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="bg-blue-700 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-800 transition-all transform hover:scale-105 shadow-xl"
|
||||||
|
>
|
||||||
|
Mulai Sekarang
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="#features"
|
||||||
|
className="border-2 border-blue-700 text-blue-700 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-700 hover:text-white transition-all transform hover:scale-105"
|
||||||
|
>
|
||||||
|
Pelajari Lebih Lanjut
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<section id="features" className="py-16 bg-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-4xl font-bold text-gray-800 mb-4">Fitur Unggulan</h2>
|
||||||
|
<p className="text-xl text-gray-600">Solusi lengkap untuk kebutuhan manajemen sekolah Anda</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{/* Feature 1 */}
|
||||||
|
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
|
||||||
|
<div className="w-16 h-16 bg-blue-700 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
|
||||||
|
<svg className="w-8 h-8 text-white" 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>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 mb-4">Manajemen Siswa</h3>
|
||||||
|
<p className="text-gray-600">Kelola data siswa, absensi, dan nilai dengan mudah dan terorganisir</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 2 */}
|
||||||
|
<div className="text-center p-8 rounded-xl bg-indigo-50 hover:bg-indigo-100 transition-colors border border-indigo-200">
|
||||||
|
<div className="w-16 h-16 bg-indigo-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
|
||||||
|
<svg className="w-8 h-8 text-white" 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>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 mb-4">Dashboard Analytics</h3>
|
||||||
|
<p className="text-gray-600">Analisis dan laporan real-time untuk memantau perkembangan sekolah</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 3 */}
|
||||||
|
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
|
||||||
|
<div className="w-16 h-16 bg-blue-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 mb-4">Keamanan Data</h3>
|
||||||
|
<p className="text-gray-600">Sistem keamanan tingkat tinggi untuk melindungi data sekolah Anda</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* About Section */}
|
||||||
|
<section id="about" className="py-16 bg-gray-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-4xl font-bold text-gray-800 mb-6">Tentang SIPINTAR</h2>
|
||||||
|
<p className="text-lg text-gray-600 mb-6">
|
||||||
|
SIPINTAR (Sistem Pemantauan Interaktif dan Pintar) adalah solusi digital terdepan untuk manajemen sekolah modern.
|
||||||
|
Dirancang khusus untuk membantu institusi pendidikan dalam mengelola data siswa, guru, dan
|
||||||
|
administrasi sekolah dengan lebih efisien.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-6 h-6 bg-blue-700 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-700">Interface yang user-friendly</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-6 h-6 bg-blue-700 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-700">Laporan otomatis dan real-time</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-6 h-6 bg-blue-700 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-700">Akses multi-platform</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="relative">
|
||||||
|
<Image
|
||||||
|
src="/Logo Geometris dengan Topi Wisuda.png"
|
||||||
|
alt="SIPINTAR Logo Large"
|
||||||
|
width={300}
|
||||||
|
height={300}
|
||||||
|
className="drop-shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-16 bg-blue-700">
|
||||||
|
<div className="max-w-7xl mx-auto px-6 text-center">
|
||||||
|
<h2 className="text-4xl font-bold text-white mb-6">Siap Memulai?</h2>
|
||||||
|
<p className="text-xl text-blue-100 mb-8 max-w-2xl mx-auto">Bergabunglah dengan sekolah-sekolah yang telah merasakan kemudahan manajemen dengan SIPINTAR</p>
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="inline-block bg-white text-blue-700 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-50 transition-all transform hover:scale-105 shadow-xl"
|
||||||
|
>
|
||||||
|
Coba Sekarang Gratis
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact Section */}
|
||||||
|
<section id="contact" className="py-16 bg-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-4xl font-bold text-gray-800 mb-4">Hubungi Kami</h2>
|
||||||
|
<p className="text-xl text-gray-600">Tim kami siap membantu Anda memulai digitalisasi sekolah</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
|
||||||
|
<div className="w-16 h-16 bg-blue-700 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 mb-4">Email</h3>
|
||||||
|
<p className="text-gray-600">info@sipintar.com</p>
|
||||||
|
<p className="text-gray-600">support@sipintar.com</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-8 rounded-xl bg-indigo-50 hover:bg-indigo-100 transition-colors border border-indigo-200">
|
||||||
|
<div className="w-16 h-16 bg-indigo-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 mb-4">Telepon</h3>
|
||||||
|
<p className="text-gray-600">+62 21 1234-5678</p>
|
||||||
|
<p className="text-gray-600">+62 812 3456-7890</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
|
||||||
|
<div className="w-16 h-16 bg-blue-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 mb-4">Alamat</h3>
|
||||||
|
<p className="text-gray-600">Jl. Pendidikan No. 123</p>
|
||||||
|
<p className="text-gray-600">Jakarta Selatan 12345</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-gray-900 py-12">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center">
|
||||||
|
<div className="flex items-center space-x-3 mb-4 md:mb-0">
|
||||||
|
<Image
|
||||||
|
src="/Logo Geometris dengan Topi Wisuda.png"
|
||||||
|
alt="SIPINTAR Logo"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<span className="text-xl font-bold text-white">SIPINTAR</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-center md:text-right">
|
||||||
|
<p>© 2025 SIPINTAR. Semua hak dilindungi.</p>
|
||||||
|
<p className="mt-2 text-sm">Sistem Informasi Pintar untuk Sekolah Modern</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
export async function hashPassword(password: string): Promise<string> {
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
const salt = await bcrypt.genSalt(12)
|
||||||
return await bcrypt.hash(password, 12)
|
return await bcrypt.hash(password, 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
declare module 'prom-client' {
|
||||||
|
export class Registry {
|
||||||
|
metrics(): Promise<string>;
|
||||||
|
contentType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectDefaultMetrics(opts: { register: Registry }): void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,403 @@
|
||||||
|
{
|
||||||
|
"SchemaVersion": 2,
|
||||||
|
"CreatedAt": "2025-08-18T15:58:35.089280383Z",
|
||||||
|
"ArtifactName": "sipintar-app",
|
||||||
|
"ArtifactType": "container_image",
|
||||||
|
"Metadata": {
|
||||||
|
"Size": 1139341312,
|
||||||
|
"OS": {
|
||||||
|
"Family": "alpine",
|
||||||
|
"Name": "3.22.1"
|
||||||
|
},
|
||||||
|
"ImageID": "sha256:4422c1774f227fddf9fa7eb97a50bfbd03ac96ca2060d64c918486d8d3c70fb6",
|
||||||
|
"DiffIDs": [
|
||||||
|
"sha256:418dccb7d85a63a6aa574439840f7a6fa6fd2321b3e2394568a317735e867d35",
|
||||||
|
"sha256:54e9a650f7b7eb822819549a555f1da682a949a2f834ee90538cdcf2c5b4e72c",
|
||||||
|
"sha256:2e12e022f9bc7fa1e7abcaaa1ca88eed4ec895ff88a92da70f4fc79360e22600",
|
||||||
|
"sha256:eadc3b779d42558c703232696a3b9f95793b1aff49f10b11991712a7710460f1",
|
||||||
|
"sha256:54f04cfbae253965e994b8b7afbf8d925ff78a4d9782dc8beea35eedd5a97146",
|
||||||
|
"sha256:52de82b62fcee570f16bd4a4cace3a61a9a2c40e01c16ca020a53f77542529cd",
|
||||||
|
"sha256:cedd4988c45e525dae0c0126cc5f31a60307e5b2c4c58dde3e22d408ee6ddfcc",
|
||||||
|
"sha256:84216d29d7b76773d49638b5b51c8d8b19fa99088aa7dc93b048e335bcbe777f",
|
||||||
|
"sha256:498f30cdf8d028e8584de14df30886e071ef725a05e6eedd1408f4dc610e6886",
|
||||||
|
"sha256:0d241dc8aff74030e8c1293f46143cd369ee252712b605f2ed7fbdc43fa0e5a7",
|
||||||
|
"sha256:2fa6215ab739c975fdfb47cd629833d744397135088f968ca4ac97998f87ecb9",
|
||||||
|
"sha256:de4d3153fae86c6cac0e472b54585fb9ea675ce0e9b0d4865d8e6754a26d5668",
|
||||||
|
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||||
|
],
|
||||||
|
"RepoTags": [
|
||||||
|
"adelyao/sipintar-app:latest",
|
||||||
|
"sipintar-app:latest"
|
||||||
|
],
|
||||||
|
"RepoDigests": [
|
||||||
|
"adelyao/sipintar-app@sha256:4422c1774f227fddf9fa7eb97a50bfbd03ac96ca2060d64c918486d8d3c70fb6",
|
||||||
|
"sipintar-app@sha256:4422c1774f227fddf9fa7eb97a50bfbd03ac96ca2060d64c918486d8d3c70fb6"
|
||||||
|
],
|
||||||
|
"ImageConfig": {
|
||||||
|
"architecture": "amd64",
|
||||||
|
"created": "2025-08-18T09:26:53.256765583Z",
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T11:01:16Z",
|
||||||
|
"created_by": "ADD alpine-minirootfs-3.22.1-x86_64.tar.gz / # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T11:01:16Z",
|
||||||
|
"created_by": "CMD [\"/bin/sh\"]",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T23:35:30Z",
|
||||||
|
"created_by": "ENV NODE_VERSION=20.19.4",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T23:35:30Z",
|
||||||
|
"created_by": "RUN /bin/sh -c addgroup -g 1000 node \u0026\u0026 adduser -u 1000 -G node -s /bin/sh -D node \u0026\u0026 apk add --no-cache libstdc++ \u0026\u0026 apk add --no-cache --virtual .build-deps curl \u0026\u0026 ARCH= OPENSSL_ARCH='linux*' \u0026\u0026 alpineArch=\"$(apk --print-arch)\" \u0026\u0026 case \"${alpineArch##*-}\" in x86_64) ARCH='x64' CHECKSUM=\"8a4633a9f8101de6870f7d4e5ceb3aa83d3c6cd7c11ad91cd902ea223b8c55fe\" OPENSSL_ARCH=linux-x86_64;; x86) OPENSSL_ARCH=linux-elf;; aarch64) OPENSSL_ARCH=linux-aarch64;; arm*) OPENSSL_ARCH=linux-armv4;; ppc64le) OPENSSL_ARCH=linux-ppc64le;; s390x) OPENSSL_ARCH=linux-s390x;; *) ;; esac \u0026\u0026 if [ -n \"${CHECKSUM}\" ]; then set -eu; curl -fsSLO --compressed \"https://unofficial-builds.nodejs.org/download/release/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\"; echo \"$CHECKSUM node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\" | sha256sum -c - \u0026\u0026 tar -xJf \"node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\" -C /usr/local --strip-components=1 --no-same-owner \u0026\u0026 ln -s /usr/local/bin/node /usr/local/bin/nodejs; else echo \"Building from source\" \u0026\u0026 apk add --no-cache --virtual .build-deps-full binutils-gold g++ gcc gnupg libgcc linux-headers make python3 py-setuptools \u0026\u0026 export GNUPGHOME=\"$(mktemp -d)\" \u0026\u0026 for key in C0D6248439F1D5604AAFFB4021D900FFDB233756 DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7 CC68F5A3106FF448322E48ED27F5E38D5B0A215F 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4 C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C 108F52B48DB57BB0CC439B2997B01419BD92F80A A363A499291CBBC940DD62E41F10027AF002F8B0 ; do { gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" \u0026\u0026 gpg --batch --fingerprint \"$key\"; } || { gpg --batch --keyserver keyserver.ubuntu.com --recv-keys \"$key\" \u0026\u0026 gpg --batch --fingerprint \"$key\"; } ; done \u0026\u0026 curl -fsSLO --compressed \"https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION.tar.xz\" \u0026\u0026 curl -fsSLO --compressed \"https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc\" \u0026\u0026 gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \u0026\u0026 gpgconf --kill all \u0026\u0026 rm -rf \"$GNUPGHOME\" \u0026\u0026 grep \" node-v$NODE_VERSION.tar.xz\\$\" SHASUMS256.txt | sha256sum -c - \u0026\u0026 tar -xf \"node-v$NODE_VERSION.tar.xz\" \u0026\u0026 cd \"node-v$NODE_VERSION\" \u0026\u0026 ./configure \u0026\u0026 make -j$(getconf _NPROCESSORS_ONLN) V= \u0026\u0026 make install \u0026\u0026 apk del .build-deps-full \u0026\u0026 cd .. \u0026\u0026 rm -Rf \"node-v$NODE_VERSION\" \u0026\u0026 rm \"node-v$NODE_VERSION.tar.xz\" SHASUMS256.txt.asc SHASUMS256.txt; fi \u0026\u0026 rm -f \"node-v$NODE_VERSION-linux-$ARCH-musl.tar.xz\" \u0026\u0026 find /usr/local/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name \"$OPENSSL_ARCH\" -exec rm -rf {} \\; \u0026\u0026 apk del .build-deps \u0026\u0026 node --version \u0026\u0026 npm --version \u0026\u0026 rm -rf /tmp/* # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T23:35:30Z",
|
||||||
|
"created_by": "ENV YARN_VERSION=1.22.22",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T23:35:30Z",
|
||||||
|
"created_by": "RUN /bin/sh -c apk add --no-cache --virtual .build-deps-yarn curl gnupg tar \u0026\u0026 export GNUPGHOME=\"$(mktemp -d)\" \u0026\u0026 for key in 6A010C5166006599AA17F08146C2130DFD2497F5 ; do { gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$key\" \u0026\u0026 gpg --batch --fingerprint \"$key\"; } || { gpg --batch --keyserver keyserver.ubuntu.com --recv-keys \"$key\" \u0026\u0026 gpg --batch --fingerprint \"$key\"; } ; done \u0026\u0026 curl -fsSLO --compressed \"https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz\" \u0026\u0026 curl -fsSLO --compressed \"https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz.asc\" \u0026\u0026 gpg --batch --verify yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz \u0026\u0026 gpgconf --kill all \u0026\u0026 rm -rf \"$GNUPGHOME\" \u0026\u0026 mkdir -p /opt \u0026\u0026 tar -xzf yarn-v$YARN_VERSION.tar.gz -C /opt/ \u0026\u0026 ln -s /opt/yarn-v$YARN_VERSION/bin/yarn /usr/local/bin/yarn \u0026\u0026 ln -s /opt/yarn-v$YARN_VERSION/bin/yarnpkg /usr/local/bin/yarnpkg \u0026\u0026 rm yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz \u0026\u0026 apk del .build-deps-yarn \u0026\u0026 yarn --version \u0026\u0026 rm -rf /tmp/* # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T23:35:30Z",
|
||||||
|
"created_by": "COPY docker-entrypoint.sh /usr/local/bin/ # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T23:35:30Z",
|
||||||
|
"created_by": "ENTRYPOINT [\"docker-entrypoint.sh\"]",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-07-15T23:35:30Z",
|
||||||
|
"created_by": "CMD [\"node\"]",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-13T01:47:32Z",
|
||||||
|
"created_by": "WORKDIR /app",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-17T02:45:33Z",
|
||||||
|
"created_by": "RUN /bin/sh -c addgroup -S appgroup \u0026\u0026 adduser -S appuser -G appgroup # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:20:22Z",
|
||||||
|
"created_by": "COPY package*.json ./ # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:42Z",
|
||||||
|
"created_by": "COPY /app/.next ./.next # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:42Z",
|
||||||
|
"created_by": "COPY /app/public ./public # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:42Z",
|
||||||
|
"created_by": "COPY /app/prisma ./prisma # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:47Z",
|
||||||
|
"created_by": "COPY /app/node_modules ./node_modules # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:47Z",
|
||||||
|
"created_by": "COPY /app/src ./src # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:53Z",
|
||||||
|
"created_by": "COPY --chown=appuser:appgroup /app/node_modules ./node_modules # buildkit",
|
||||||
|
"comment": "buildkit.dockerfile.v0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:53Z",
|
||||||
|
"created_by": "ENV NODE_ENV=production",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:53Z",
|
||||||
|
"created_by": "EXPOSE map[3000/tcp:{}]",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:53Z",
|
||||||
|
"created_by": "USER appuser",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2025-08-18T09:26:53Z",
|
||||||
|
"created_by": "CMD [\"npm\" \"run\" \"start\"]",
|
||||||
|
"comment": "buildkit.dockerfile.v0",
|
||||||
|
"empty_layer": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"os": "linux",
|
||||||
|
"rootfs": {
|
||||||
|
"type": "layers",
|
||||||
|
"diff_ids": [
|
||||||
|
"sha256:418dccb7d85a63a6aa574439840f7a6fa6fd2321b3e2394568a317735e867d35",
|
||||||
|
"sha256:54e9a650f7b7eb822819549a555f1da682a949a2f834ee90538cdcf2c5b4e72c",
|
||||||
|
"sha256:2e12e022f9bc7fa1e7abcaaa1ca88eed4ec895ff88a92da70f4fc79360e22600",
|
||||||
|
"sha256:eadc3b779d42558c703232696a3b9f95793b1aff49f10b11991712a7710460f1",
|
||||||
|
"sha256:54f04cfbae253965e994b8b7afbf8d925ff78a4d9782dc8beea35eedd5a97146",
|
||||||
|
"sha256:52de82b62fcee570f16bd4a4cace3a61a9a2c40e01c16ca020a53f77542529cd",
|
||||||
|
"sha256:cedd4988c45e525dae0c0126cc5f31a60307e5b2c4c58dde3e22d408ee6ddfcc",
|
||||||
|
"sha256:84216d29d7b76773d49638b5b51c8d8b19fa99088aa7dc93b048e335bcbe777f",
|
||||||
|
"sha256:498f30cdf8d028e8584de14df30886e071ef725a05e6eedd1408f4dc610e6886",
|
||||||
|
"sha256:0d241dc8aff74030e8c1293f46143cd369ee252712b605f2ed7fbdc43fa0e5a7",
|
||||||
|
"sha256:2fa6215ab739c975fdfb47cd629833d744397135088f968ca4ac97998f87ecb9",
|
||||||
|
"sha256:de4d3153fae86c6cac0e472b54585fb9ea675ce0e9b0d4865d8e6754a26d5668",
|
||||||
|
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"Cmd": [
|
||||||
|
"npm",
|
||||||
|
"run",
|
||||||
|
"start"
|
||||||
|
],
|
||||||
|
"Entrypoint": [
|
||||||
|
"docker-entrypoint.sh"
|
||||||
|
],
|
||||||
|
"Env": [
|
||||||
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||||
|
"NODE_VERSION=20.19.4",
|
||||||
|
"YARN_VERSION=1.22.22",
|
||||||
|
"NODE_ENV=production"
|
||||||
|
],
|
||||||
|
"User": "appuser",
|
||||||
|
"WorkingDir": "/app",
|
||||||
|
"ExposedPorts": {
|
||||||
|
"3000/tcp": {}
|
||||||
|
},
|
||||||
|
"ArgsEscaped": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Layers": [
|
||||||
|
{
|
||||||
|
"Size": 8596480,
|
||||||
|
"Digest": "sha256:9824c27679d3b27c5e1cb00a73adb6f4f8d556994111c12db3c5d61a0c843df8",
|
||||||
|
"DiffID": "sha256:418dccb7d85a63a6aa574439840f7a6fa6fd2321b3e2394568a317735e867d35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 123036672,
|
||||||
|
"Digest": "sha256:8c59d92d6fc9f01af4aaa86824be72b74bd4d940c4c46aa95d9710bfa46c975e",
|
||||||
|
"DiffID": "sha256:54e9a650f7b7eb822819549a555f1da682a949a2f834ee90538cdcf2c5b4e72c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 5389824,
|
||||||
|
"Digest": "sha256:54225bd601967a0aa669ec9be621c24d8eeac874b698d55874018070898685c2",
|
||||||
|
"DiffID": "sha256:2e12e022f9bc7fa1e7abcaaa1ca88eed4ec895ff88a92da70f4fc79360e22600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 3584,
|
||||||
|
"Digest": "sha256:a9e48ad1219d4d11c6456a8db0fd5c11af46242d52edf84e17ab84a7bfd93809",
|
||||||
|
"DiffID": "sha256:eadc3b779d42558c703232696a3b9f95793b1aff49f10b11991712a7710460f1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 1536,
|
||||||
|
"Digest": "sha256:0acc2a2bdf69d278947470113f79bc624dcc672a328ccf41657d1093ea7ac237",
|
||||||
|
"DiffID": "sha256:54f04cfbae253965e994b8b7afbf8d925ff78a4d9782dc8beea35eedd5a97146"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 10752,
|
||||||
|
"Digest": "sha256:4ce2fe5b490be14137c39d7a05adddb6e9712317a651a0ba1a09170ac48bfd77",
|
||||||
|
"DiffID": "sha256:52de82b62fcee570f16bd4a4cace3a61a9a2c40e01c16ca020a53f77542529cd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 319488,
|
||||||
|
"Digest": "sha256:82027a5441a2188d23f508670e0b65191f8da4ff49120f4c902b265bd7d3c25f",
|
||||||
|
"DiffID": "sha256:cedd4988c45e525dae0c0126cc5f31a60307e5b2c4c58dde3e22d408ee6ddfcc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 134096896,
|
||||||
|
"Digest": "sha256:303f9011b20d7dc5150807034500a70a22707109da269ca736c290e4b9c22a7c",
|
||||||
|
"DiffID": "sha256:84216d29d7b76773d49638b5b51c8d8b19fa99088aa7dc93b048e335bcbe777f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 1474048,
|
||||||
|
"Digest": "sha256:ca4a39f0cf921046127ac9362d111e627f1551d9bf6806ca6668f8468392a87d",
|
||||||
|
"DiffID": "sha256:498f30cdf8d028e8584de14df30886e071ef725a05e6eedd1408f4dc610e6886"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 18432,
|
||||||
|
"Digest": "sha256:cac1fc27c7c00da22f9fadef43ae0f6f03235af5c62f115f77004d5ac7b5078d",
|
||||||
|
"DiffID": "sha256:0d241dc8aff74030e8c1293f46143cd369ee252712b605f2ed7fbdc43fa0e5a7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 866106368,
|
||||||
|
"Digest": "sha256:cfad1940bb9c092c84218657f55c636bb12b5a841a361271f6ea47cea28cc44f",
|
||||||
|
"DiffID": "sha256:2fa6215ab739c975fdfb47cd629833d744397135088f968ca4ac97998f87ecb9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 286208,
|
||||||
|
"Digest": "sha256:f3fb51f92f7844fb809251866d6af9c1cfc1d888c3d807ce966d329ad7155d45",
|
||||||
|
"DiffID": "sha256:de4d3153fae86c6cac0e472b54585fb9ea675ce0e9b0d4865d8e6754a26d5668"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Size": 1024,
|
||||||
|
"Digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
|
||||||
|
"DiffID": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Results": [
|
||||||
|
{
|
||||||
|
"Target": "sipintar-app (alpine 3.22.1)",
|
||||||
|
"Class": "os-pkgs",
|
||||||
|
"Type": "alpine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Target": "Node.js",
|
||||||
|
"Class": "lang-pkgs",
|
||||||
|
"Type": "node-pkg",
|
||||||
|
"Vulnerabilities": [
|
||||||
|
{
|
||||||
|
"VulnerabilityID": "CVE-2024-21538",
|
||||||
|
"PkgID": "cross-spawn@7.0.3",
|
||||||
|
"PkgName": "cross-spawn",
|
||||||
|
"PkgPath": "usr/local/lib/node_modules/npm/node_modules/cross-spawn/package.json",
|
||||||
|
"PkgIdentifier": {
|
||||||
|
"PURL": "pkg:npm/cross-spawn@7.0.3",
|
||||||
|
"UID": "480da04347e2e299"
|
||||||
|
},
|
||||||
|
"InstalledVersion": "7.0.3",
|
||||||
|
"FixedVersion": "7.0.5, 6.0.6",
|
||||||
|
"Status": "fixed",
|
||||||
|
"Layer": {
|
||||||
|
"Digest": "sha256:8c59d92d6fc9f01af4aaa86824be72b74bd4d940c4c46aa95d9710bfa46c975e",
|
||||||
|
"DiffID": "sha256:54e9a650f7b7eb822819549a555f1da682a949a2f834ee90538cdcf2c5b4e72c"
|
||||||
|
},
|
||||||
|
"SeveritySource": "ghsa",
|
||||||
|
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-21538",
|
||||||
|
"DataSource": {
|
||||||
|
"ID": "ghsa",
|
||||||
|
"Name": "GitHub Security Advisory npm",
|
||||||
|
"URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm"
|
||||||
|
},
|
||||||
|
"Title": "cross-spawn: regular expression denial of service",
|
||||||
|
"Description": "Versions of the package cross-spawn before 6.0.6, from 7.0.0 and before 7.0.5 are vulnerable to Regular Expression Denial of Service (ReDoS) due to improper input sanitization. An attacker can increase the CPU usage and crash the program by crafting a very large and well crafted string.",
|
||||||
|
"Severity": "HIGH",
|
||||||
|
"CweIDs": [
|
||||||
|
"CWE-1333"
|
||||||
|
],
|
||||||
|
"VendorSeverity": {
|
||||||
|
"amazon": 2,
|
||||||
|
"azure": 3,
|
||||||
|
"cbl-mariner": 3,
|
||||||
|
"ghsa": 3,
|
||||||
|
"redhat": 1
|
||||||
|
},
|
||||||
|
"CVSS": {
|
||||||
|
"ghsa": {
|
||||||
|
"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
|
||||||
|
"V3Score": 7.5
|
||||||
|
},
|
||||||
|
"redhat": {
|
||||||
|
"V3Vector": "CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H",
|
||||||
|
"V3Score": 4.4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"References": [
|
||||||
|
"https://access.redhat.com/security/cve/CVE-2024-21538",
|
||||||
|
"https://github.com/moxystudio/node-cross-spawn",
|
||||||
|
"https://github.com/moxystudio/node-cross-spawn/commit/5ff3a07d9add449021d806e45c4168203aa833ff",
|
||||||
|
"https://github.com/moxystudio/node-cross-spawn/commit/640d391fde65388548601d95abedccc12943374f",
|
||||||
|
"https://github.com/moxystudio/node-cross-spawn/commit/d35c865b877d2f9ded7c1ed87521c2fdb689c8dd",
|
||||||
|
"https://github.com/moxystudio/node-cross-spawn/issues/165",
|
||||||
|
"https://github.com/moxystudio/node-cross-spawn/pull/160",
|
||||||
|
"https://nvd.nist.gov/vuln/detail/CVE-2024-21538",
|
||||||
|
"https://security.snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-8366349",
|
||||||
|
"https://security.snyk.io/vuln/SNYK-JS-CROSSSPAWN-8303230",
|
||||||
|
"https://www.cve.org/CVERecord?id=CVE-2024-21538"
|
||||||
|
],
|
||||||
|
"PublishedDate": "2024-11-08T05:15:06.453Z",
|
||||||
|
"LastModifiedDate": "2025-05-20T15:16:03.53Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Target": "app/node_modules/.pnpm/@esbuild+linux-x64@0.25.8/node_modules/@esbuild/linux-x64/bin/esbuild",
|
||||||
|
"Class": "lang-pkgs",
|
||||||
|
"Type": "gobinary",
|
||||||
|
"Vulnerabilities": [
|
||||||
|
{
|
||||||
|
"VulnerabilityID": "CVE-2025-47907",
|
||||||
|
"PkgID": "stdlib@v1.23.10",
|
||||||
|
"PkgName": "stdlib",
|
||||||
|
"PkgIdentifier": {
|
||||||
|
"PURL": "pkg:golang/stdlib@v1.23.10",
|
||||||
|
"UID": "195c06e8a44e6ede"
|
||||||
|
},
|
||||||
|
"InstalledVersion": "v1.23.10",
|
||||||
|
"FixedVersion": "1.23.12, 1.24.6",
|
||||||
|
"Status": "fixed",
|
||||||
|
"Layer": {
|
||||||
|
"Digest": "sha256:cfad1940bb9c092c84218657f55c636bb12b5a841a361271f6ea47cea28cc44f",
|
||||||
|
"DiffID": "sha256:2fa6215ab739c975fdfb47cd629833d744397135088f968ca4ac97998f87ecb9"
|
||||||
|
},
|
||||||
|
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2025-47907",
|
||||||
|
"DataSource": {
|
||||||
|
"ID": "govulndb",
|
||||||
|
"Name": "The Go Vulnerability Database",
|
||||||
|
"URL": "https://pkg.go.dev/vuln/"
|
||||||
|
},
|
||||||
|
"Title": "database/sql: Postgres Scan Race Condition",
|
||||||
|
"Description": "Cancelling a query (e.g. by cancelling the context passed to one of the query methods) during a call to the Scan method of the returned Rows can result in unexpected results if other queries are being made in parallel. This can result in a race condition that may overwrite the expected results with those of another query, causing the call to Scan to return either unexpected results from the other query or an error.",
|
||||||
|
"Severity": "HIGH",
|
||||||
|
"VendorSeverity": {
|
||||||
|
"bitnami": 3,
|
||||||
|
"redhat": 3
|
||||||
|
},
|
||||||
|
"CVSS": {
|
||||||
|
"bitnami": {
|
||||||
|
"V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:L",
|
||||||
|
"V3Score": 7
|
||||||
|
},
|
||||||
|
"redhat": {
|
||||||
|
"V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:L",
|
||||||
|
"V3Score": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"References": [
|
||||||
|
"https://access.redhat.com/security/cve/CVE-2025-47907",
|
||||||
|
"https://go.dev/cl/693735",
|
||||||
|
"https://go.dev/issue/74831",
|
||||||
|
"https://groups.google.com/g/golang-announce/c/x5MKroML2yM",
|
||||||
|
"https://nvd.nist.gov/vuln/detail/CVE-2025-47907",
|
||||||
|
"https://pkg.go.dev/vuln/GO-2025-3849",
|
||||||
|
"https://www.cve.org/CVERecord?id=CVE-2025-47907"
|
||||||
|
],
|
||||||
|
"PublishedDate": "2025-08-07T16:15:30.357Z",
|
||||||
|
"LastModifiedDate": "2025-08-07T21:26:37.453Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,6 @@
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src", "src/types"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue