Compare commits

..

No commits in common. "13d26079da009163330e04dcad0477ad75c0964b" and "d9392d0d1e7d96d9558621d779340289a560e98f" have entirely different histories.

43 changed files with 640 additions and 10771 deletions

View File

@ -1,49 +0,0 @@
# 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/

4
.env
View File

@ -1,4 +0,0 @@
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"

View File

@ -1,3 +0,0 @@
#DATABASE_URL="mysql://root:@localhost:3306/sipintar_school"
#NEXTAUTH_SECRET="your-secret-key-here-change-this-in-production"
#NEXTAUTH_URL="http://localhost:3000"

View File

14
.gitignore vendored
View File

@ -24,17 +24,15 @@
.DS_Store
*.pem
# local env files
.env
.env*.local
.env.prod
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
@ -43,9 +41,3 @@ yarn-error.log*
next-env.d.ts
/src/generated/prisma
#secrets
/secrets/
#trivy
/trivy-results/

View File

@ -1,17 +0,0 @@
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"]

View File

@ -1,42 +0,0 @@
# 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"]

View File

@ -1,20 +1,17 @@
# 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" />
# SIPINTAR - Sistem Informasi Pintar Sekolah
Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Prisma ORM, dan MySQL.
## ✨ Fitur Utama
- 👨‍💼 **Dashboard Pemantauan Pintar**: Kelola seluruh sistem sekolah dengan analytics
- 👨‍🎓 **Pemantauan Siswa**: CRUD siswa, enrollment kelas, tracking progress real-time
- 👨‍🏫 **Pemantauan Guru**: CRUD guru, assignment mata pelajaran, evaluasi kinerja
- 🏫 **Pemantauan Kelas**: Monitoring aktivitas kelas, wali kelas, mata pelajaran
- ⏰ **Sistem Absensi Interaktif**: Input dan monitoring kehadiran dengan alert otomatis
- 📊 **Sistem Penilaian Pintar**: Input nilai, analitik akademik, insights progress
- 👨‍💼 **Dashboard Admin**: Kelola seluruh sistem sekolah
- 👨‍🎓 **Manajemen Siswa**: CRUD siswa, enrollment kelas, data orang tua
- 👨‍🏫 **Manajemen Guru**: CRUD guru, assignment mata pelajaran, kualifikasi
- 🏫 **Manajemen Kelas**: Pembagian kelas, wali kelas, mata pelajaran
- ⏰ **Sistem Absensi**: Input dan monitoring kehadiran realtime
- 📊 **Sistem Penilaian**: Input nilai, laporan akademik, tracking progress
- 🔐 **Authentication**: Login role-based (Admin, Guru, Siswa)
- 📱 **Responsive Design**: Mobile-first approach
- 📈 **Analytics Dashboard**: Chart dan visualisasi data dengan Recharts
## 🛠️ Tech Stack
@ -23,7 +20,6 @@ Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Pris
- **Database**: MySQL dengan Prisma ORM
- **Authentication**: JWT dengan bcryptjs
- **UI Components**: Radix UI, Lucide React
- **Charts**: Recharts untuk visualisasi data
- **Styling**: Tailwind CSS
## 🚀 Quick Start
@ -39,7 +35,7 @@ Aplikasi manajemen sekolah modern yang dibangun dengan Next.js, TypeScript, Pris
1. **Clone repository** (jika dari git)
```bash
git clone <repository-url>
cd SIPINTAR
cd sipintar-app
```
2. **Install dependencies**

View File

@ -1,112 +0,0 @@
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

View File

@ -1,108 +0,0 @@
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

View File

@ -1,52 +0,0 @@
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

View File

@ -1,15 +0,0 @@
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'

View File

@ -1,54 +0,0 @@
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

View File

@ -1,25 +0,0 @@
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']

View File

@ -1,18 +0,0 @@
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

288
package-lock.json generated
View File

@ -41,9 +41,8 @@
"eslint": "^9",
"eslint-config-next": "15.4.4",
"tailwindcss": "^4",
"ts-node": "^10.9.2",
"tsx": "^4.20.3",
"typescript": "^5.9.2"
"typescript": "^5"
}
},
"node_modules/@alloc/quick-lru": {
@ -82,30 +81,6 @@
"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": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
@ -2655,34 +2630,6 @@
"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": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
@ -2836,17 +2783,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/type-utils": "8.39.1",
"@typescript-eslint/utils": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/type-utils": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -2860,9 +2807,9 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.39.1",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
@ -2876,16 +2823,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4"
},
"engines": {
@ -2897,18 +2844,18 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.1",
"@typescript-eslint/types": "^8.39.1",
"@typescript-eslint/tsconfig-utils": "^8.38.0",
"@typescript-eslint/types": "^8.38.0",
"debug": "^4.3.4"
},
"engines": {
@ -2919,18 +2866,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1"
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2941,9 +2888,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -2954,19 +2901,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/utils": "8.39.1",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -2979,13 +2926,13 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
"dev": true,
"license": "MIT",
"engines": {
@ -2997,16 +2944,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.1",
"@typescript-eslint/tsconfig-utils": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"@typescript-eslint/project-service": "8.38.0",
"@typescript-eslint/tsconfig-utils": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -3022,7 +2969,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
@ -3082,16 +3029,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1"
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3102,17 +3049,17 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/types": "8.38.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@ -3415,19 +3362,6 @@
"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": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -3461,13 +3395,6 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -3960,13 +3887,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": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -4263,16 +4183,6 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@ -6525,13 +6435,6 @@
"@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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -8209,50 +8112,6 @@
"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": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@ -8384,9 +8243,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@ -8528,13 +8387,6 @@
"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": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
@ -8682,16 +8534,6 @@
"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": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -45,8 +45,7 @@
"eslint": "^9",
"eslint-config-next": "15.4.4",
"tailwindcss": "^4",
"ts-node": "^10.9.2",
"tsx": "^4.20.3",
"typescript": "^5.9.2"
"typescript": "^5"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -36,13 +36,6 @@ enum Role {
ADMIN
TEACHER
STUDENT
PARENT
}
enum Gender {
MALE
FEMALE
OTHER
}
// Model untuk Siswa
@ -51,7 +44,6 @@ model Student {
userId String @unique
studentNumber String @unique
dateOfBirth DateTime
gender Gender @default(OTHER)
address String
phone String?
parentName String

View File

@ -17,17 +17,6 @@ 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
const teacherPassword = await hashPassword('guru123')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -4,11 +4,11 @@ mysql -u root -p
# Buat database baru
CREATE DATABASE sipintar_school CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# Buat user khusus
CREATE USER 'sipintar_user'@'%' IDENTIFIED BY 'sipintar_password123';
# Buat user khusus (opsional tapi recommended)
CREATE USER 'sipintar_user'@'localhost' IDENTIFIED BY 'sipintar_password123';
# Berikan akses ke database
GRANT ALL PRIVILEGES ON sipintar_school.* TO 'sipintar_user'@'%';
GRANT ALL PRIVILEGES ON sipintar_school.* TO 'sipintar_user'@'localhost';
# Refresh privileges
FLUSH PRIVILEGES;

View File

@ -1,191 +0,0 @@
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 }
)
}
}

View File

@ -23,9 +23,9 @@ export async function GET(request: NextRequest) {
name: string
}
if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
if (decoded.role !== 'ADMIN') {
return NextResponse.json(
{ message: 'Access denied. Admin or Teacher role required.' },
{ message: 'Access denied. Admin role required.' },
{ status: 403 }
)
}
@ -38,49 +38,11 @@ export async function GET(request: NextRequest) {
prisma.subject.count({ where: { isActive: true } }),
])
// Get class distribution by grade and section
const classDistribution = await prisma.class.groupBy({
by: ['grade', 'section'],
where: { isActive: true },
_count: {
id: true,
},
})
// Format class distribution data
const classByGrade = classDistribution.reduce((acc: Record<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({
totalStudents,
totalTeachers,
totalClasses,
totalSubjects,
studentsByGender: genderStats,
classByGrade,
classBySection,
})
} catch (error) {
console.error('Dashboard stats error:', error)

View File

@ -1,11 +0,0 @@
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 },
});
}

View File

@ -1,6 +0,0 @@
{
"name": "api",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

View File

@ -1,172 +0,0 @@
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 }
)
}
}

View File

@ -1,192 +0,0 @@
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 }
)
}
}

View File

@ -27,18 +27,9 @@ export default function LoginPage() {
if (response.ok) {
const data = await response.json()
// Store token and user data
// Store token or handle authentication
localStorage.setItem('token', data.token)
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
}
router.push('/dashboard')
} else {
const data = await response.json()
setError(data.message || 'Login failed')
@ -80,7 +71,7 @@ export default function LoginPage() {
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 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 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Masukkan email Anda"
/>
</div>
@ -96,7 +87,7 @@ export default function LoginPage() {
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 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 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Masukkan password Anda"
/>
</div>
@ -146,8 +137,9 @@ export default function LoginPage() {
<div className="mt-8 p-4 bg-gray-50 rounded-md">
<h4 className="text-sm font-medium text-gray-900 mb-2">Demo Accounts:</h4>
<div className="text-xs text-gray-600 space-y-1">
<p><strong>Admin (Guru):</strong> admin@sipintar.com / admin123</p>
<p><strong>Orang Tua:</strong> parent@sipintar.com / parent123</p>
<p><strong>Admin:</strong> admin@sipintar.com / admin123</p>
<p><strong>Guru:</strong> guru@sipintar.com / guru123</p>
<p><strong>Siswa:</strong> siswa@sipintar.com / siswa123</p>
</div>
</div>
</div>

View File

@ -1,681 +0,0 @@
'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>
)
}

View File

@ -1,448 +0,0 @@
'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>
)
}

View File

@ -1,6 +0,0 @@
{
"name": "dashboard",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

View File

@ -3,556 +3,426 @@
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
LineChart,
Line,
PieChart,
Pie,
Cell,
} from 'recharts'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from 'recharts'
interface User {
id: string
email: string
name: string
role: string
profileImage?: string
studentId?: string
teacherId?: string
id: string
email: string
name: string
role: string
profileImage?: string
studentId?: string
teacherId?: string
}
interface DashboardStats {
totalStudents: number
totalTeachers: number
totalClasses: number
totalSubjects: number
totalStudents: number
totalTeachers: number
totalClasses: number
totalSubjects: number
}
interface ChartData {
enrollmentData: { name: string; students: number }[]
attendanceData: {
month: string
present: number
absent: number
late: number
}[]
gradeData: { grade: string; count: number }[]
subjectData: { name: string; teachers: number }[]
enrollmentData: { name: string; students: number }[]
attendanceData: { month: string; present: number; absent: number; late: number }[]
gradeData: { grade: string; count: number }[]
subjectData: { name: string; teachers: number }[]
}
export default function DashboardPage() {
const [user, setUser] = useState<User | null>(null)
const [stats, setStats] = useState<DashboardStats | null>(null)
const [chartData, setChartData] = useState<ChartData | null>(null)
const [isLoading, setIsLoading] = useState(true)
const router = useRouter()
const [user, setUser] = useState<User | null>(null)
const [stats, setStats] = useState<DashboardStats | null>(null)
const [chartData, setChartData] = useState<ChartData | null>(null)
const [isLoading, setIsLoading] = useState(true)
const router = useRouter()
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/auth/login')
return
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/auth/login')
return
}
// Parse JWT to get user info (in production, verify with server)
try {
const payload = JSON.parse(atob(token.split('.')[1]))
setUser(payload)
fetchDashboardData()
} catch (error) {
console.error('Invalid token:', error)
localStorage.removeItem('token')
router.push('/auth/login')
}
}, [router])
const fetchDashboardData = async () => {
try {
const token = localStorage.getItem('token')
// Fetch stats
const statsResponse = await fetch('/api/dashboard/stats', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (statsResponse.ok) {
const statsData = await statsResponse.json()
setStats(statsData)
}
// Fetch chart data
const chartsResponse = await fetch('/api/dashboard/charts', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (chartsResponse.ok) {
const chartsData = await chartsResponse.json()
setChartData(chartsData)
}
} catch (error) {
console.error('Failed to fetch dashboard data:', error)
} finally {
setIsLoading(false)
}
}
// 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')
const handleLogout = () => {
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)
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>
)
}
}
const handleLogout = () => {
localStorage.removeItem('token')
router.push('/auth/login')
}
if (isLoading) {
return (
<div className='min-h-screen flex items-center justify-center'>
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600'></div>
</div>
)
}
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">SIPINTAR</h1>
<span className="ml-2 text-sm text-gray-500">Dashboard</span>
</div>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700">
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
</span>
<button
onClick={handleLogout}
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Logout
</button>
</div>
</div>
</div>
</nav>
// 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
<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>
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>
{/* 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>
</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...
}

View File

@ -1,263 +0,0 @@
'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>
)
}

View File

@ -21,19 +21,6 @@ export default function StudentsPage() {
const [students, setStudents] = useState<Student[]>([])
const [isLoading, setIsLoading] = useState(true)
const [showAddForm, setShowAddForm] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
studentNumber: '',
dateOfBirth: '',
address: '',
phone: '',
parentName: '',
parentPhone: '',
emergencyContact: '',
})
const router = useRouter()
useEffect(() => {
@ -66,61 +53,6 @@ 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) {
return (
<div className="min-h-screen flex items-center justify-center">
@ -141,7 +73,7 @@ export default function StudentsPage() {
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => router.push('/dashboard/admin')}
onClick={() => router.push('/dashboard')}
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Kembali ke Dashboard
@ -253,179 +185,18 @@ export default function StudentsPage() {
{/* Add Student Modal */}
{showAddForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-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 Siswa Baru</h3>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Data Siswa */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Siswa</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-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 className="bg-white rounded-lg max-w-md w-full p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Tambah Siswa Baru</h3>
<p className="text-sm text-gray-600 mb-4">
Fitur ini akan segera tersedia. Gunakan API endpoint /api/students untuk menambah siswa.
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowAddForm(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
>
Tutup
</button>
</div>
</div>
</div>

View File

@ -1,432 +0,0 @@
'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>
)
}

View File

@ -1,6 +0,0 @@
{
"name": "app",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

View File

@ -1,5 +1,142 @@
import LandingPage from '@/components/LandingPage'
import Link from "next/link";
export default function Home() {
return <LandingPage />
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Navigation */}
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">SIPINTAR</h1>
<span className="ml-2 text-sm text-gray-500">Sistem 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>
);
}

View File

@ -1,254 +0,0 @@
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>&copy; 2025 SIPINTAR. Semua hak dilindungi.</p>
<p className="mt-2 text-sm">Sistem Informasi Pintar untuk Sekolah Modern</p>
</div>
</div>
</div>
</footer>
</div>
)
}

View File

@ -1,7 +1,6 @@
import bcrypt from 'bcryptjs'
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(12)
return await bcrypt.hash(password, 12)
}

View File

@ -1,8 +0,0 @@
declare module 'prom-client' {
export class Registry {
metrics(): Promise<string>;
contentType: string;
}
export function collectDefaultMetrics(opts: { register: Registry }): void;
}

View File

@ -1,403 +0,0 @@
{
"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"
}
]
}
]
}

View File

@ -22,6 +22,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src", "src/types"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}