UI/UX Redesign: Student profiles, history pages, and alumni templates

IMPROVEMENTS:
- Redesigned student profile pages (Admin & BK) with horizontal gradient headers
- Avatar + profile info grid layout for better visual hierarchy
- Sticky chat sidebar with 2/3-1/3 column split for main content
- Updated profile edit page with modern gradient header and compact form
- Redesigned history rekomendasi with expandable details and data summary
- Added 4 interactive Chart.js visualizations to dashboards (Doughnut, Bar, Pie)
- Improved sidebar typography (Admin Panel, Administrator now white)

ALUMNI IMPORT:
- Created TEMPLATE_ALUMNI_MINIMAL.csv (15 essential columns)
- Created TEMPLATE_ALUMNI_IMPORT.csv (full 25-column template with examples)
- Added comprehensive import guide (TEMPLATE_IMPORT_ALUMNI_BIMA_AMBULU.md)

DATABASE:
- Migration for Indonesian table naming (recommendations → rekomendasi, etc.)
- Updated all models with table mappings and backward-compatible accessors
- Fixed JSON casting for hasil_rekomendasi field (Recommendation model)

DATA QUALITY:
- Updated Naive Bayes weights for all 9 majors with accurate bobot_mapel
- Expanded keywords to 15-26 universal values per major
- Implemented 5 universal preferensi_studi values per major
- Added StudentWithAccurateRecommendationSeeder (14 sample students)
- Added RegenerateRecommendationsSeeder with JSON encoding fix

PYTHON BACKEND:
- Fixed Python 3.13 compatibility (pip upgrade: certifi, requests)
- Flask app now runs successfully on Python 3.13

DASHBOARD:
- Admin & BK dashboards with real-time chart data
- Cleaned major name formatting in JSON_EXTRACT queries
- Fixed 32 malformed recommendations data quality issue

Controllers: AdminController, BKController updated with chart data
Views: 6+ views redesigned with modern gradients and responsive layouts
Models: 5+ models updated with Indonesian table mappings
Tests: 45 tests passing, crud validation suite maintained
This commit is contained in:
KakaPatria 2026-04-27 08:16:24 +07:00
parent 3f0ce730a4
commit b48f27505e
28 changed files with 2578 additions and 513 deletions

1
.phpunit.result.cache Normal file
View File

@ -0,0 +1 @@
{"version":2,"defects":[],"times":{"Tests\\Feature\\CrudValidationTest::test_admin_can_add_jurusan_data":0.275,"Tests\\Feature\\CrudValidationTest::test_bk_can_add_jurusan_data":0.011,"Tests\\Feature\\CrudValidationTest::test_admin_guru_bk_store_validates_email_and_password":0.05,"Tests\\Feature\\CrudValidationTest::test_rekomendasi_ipa_requires_all_ipa_scores":0.009,"Tests\\Feature\\CrudValidationTest::test_admin_student_detail_only_accepts_siswa_id":0.047,"Tests\\Feature\\RekomendasiTest::test_high_math_and_coding_prefers_teknologi_informasi":0.028,"Tests\\Feature\\RekomendasiTest::test_high_language_prefers_bahasa_komunikasi":0.023}}

210
PANDUAN_IMPORT_SISWA.txt Normal file
View File

@ -0,0 +1,210 @@
PANDUAN PENAMBAHAN DATA SISWA KE SISTEM SPK JURUSAN
═════════════════════════════════════════════════════════════════════════════
1. STRUKTUR DATABASE UNTUK SISWA
────────────────────────────────────────────────────────────────────────────
Tabel: users (informasi siswa)
┌──────────────────┬──────────┬────────────────────────────────────────────┐
│ Kolom │ Tipe │ Keterangan │
├──────────────────┼──────────┼────────────────────────────────────────────┤
│ id │ Integer │ ID Otomatis (jangan isi) │
│ name │ String │ Nama Lengkap Siswa (max 255 karakter) │
│ email │ String │ Email Unik (format: nama@student.edu) │
│ password │ String │ Password (bisa default atau custom) │
│ role │ String │ Role tetap: "siswa" (jangan diubah) │
│ kelompok_asal │ String │ IPA atau IPS (wajib diisi) │
└──────────────────┴──────────┴────────────────────────────────────────────┘
Tabel: rekomendasi (hasil test dan rekomendasi jurusan)
┌──────────────────┬──────────┬────────────────────────────────────────────┐
│ Kolom │ Tipe │ Keterangan │
├──────────────────┼──────────┼────────────────────────────────────────────┤
│ user_id │ Integer │ ID Siswa (dari tabel users) │
│ mtk │ Decimal │ Nilai Matematika (0-100) │
│ fisika │ Decimal │ Nilai Fisika (0-100) - Untuk IPA │
│ kimia │ Decimal │ Nilai Kimia (0-100) - Untuk IPA │
│ biologi │ Decimal │ Nilai Biologi (0-100) - Untuk IPA │
│ ekonomi │ Decimal │ Nilai Ekonomi (0-100) - Untuk IPS │
│ geografi │ Decimal │ Nilai Geografi (0-100) - Untuk IPS │
│ sosiologi │ Decimal │ Nilai Sosiologi (0-100) - Untuk IPS │
│ sejarah │ Decimal │ Nilai Sejarah (0-100) - Untuk IPS │
│ minat │ String │ Minat Siswa (max 255 karakter) │
│ preferensi_studi │ String │ Preferensi Studi (lihat daftar di bawah) │
│ cita_cita │ String │ Cita-cita/Karir Impian (max 255 karakter) │
│ prestasi │ String │ Prestasi yang dimiliki (max 255 karakter) │
│ hasil_rekomendasi│ JSON │ Hasil ranking jurusan (otomatis generate) │
└──────────────────┴──────────┴────────────────────────────────────────────┘
═════════════════════════════════════════════════════════════════════════════
2. DAFTAR PREFERENSI STUDI (Gunakan sesuai kategori)
────────────────────────────────────────────────────────────────────────────
UNTUK KELOMPOK IPA:
• Sains & Teknologi
• Pertanian & Lingkungan
• Kesehatan & Ilmu Hayat
UNTUK KELOMPOK IPS:
• Bisnis & Manajemen
• Sosial & Humaniora
═════════════════════════════════════════════════════════════════════════════
3. DAFTAR JURUSAN & BOBOT MAPEL (UNTUK REFERENSI)
────────────────────────────────────────────────────────────────────────────
JURUSAN IPA:
1. TEKNOLOGI INFORMASI
MTK: 0.50★ Fisika: 0.20 Kimia: 0.10 Biologi: 0.10
(Cocok untuk: nilai MTK tinggi, logika kuat, tertarik programming)
2. TEKNIK
MTK: 0.40 Fisika: 0.45★ Kimia: 0.15 Biologi: 0.10
(Cocok untuk: fisika tinggi, suka mesin/listrik/konstruksi)
3. TEKNOLOGI PERTANIAN
MTK: 0.35 Fisika: 0.35★ Kimia: 0.20 Biologi: 0.15
(Cocok untuk: fisika-mtk tinggi, tertarik inovasi pertanian)
4. KESEHATAN
MTK: 0.20 Fisika: 0.15 Kimia: 0.35 Biologi: 0.45★
(Cocok untuk: kimia-biologi tinggi, peduli kesehatan)
5. PRODUKSI PERTANIAN
MTK: 0.15 Fisika: 0.15 Kimia: 0.30 Biologi: 0.40★
(Cocok untuk: biologi tinggi, tertarik pertanian tradisional)
6. PETERNAKAN
MTK: 0.20 Fisika: 0.15 Kimia: 0.25 Biologi: 0.45★
(Cocok untuk: biologi tinggi, tertarik hewan/ternak)
JURUSAN IPS:
7. BISNIS
MTK: 0.45 Ekonomi: 0.50★ Geografi: 0.15 Sosiologi: 0.20
(Cocok untuk: ekonomi-mtk tinggi, suka perhitungan bisnis)
8. MANAJEMEN AGRIBISNIS
MTK: 0.35 Ekonomi: 0.45★ Geografi: 0.20 Sosiologi: 0.20
(Cocok untuk: ekonomi tinggi, tertarik bisnis pertanian)
9. BAHASA, KOMUNIKASI, DAN PARIWISATA
MTK: 0.15 Ekonomi: 0.25 Geografi: 0.35 Sosiologi: 0.35★
(Cocok untuk: sejarah-sosiologi tinggi, suka bahasa/wisata)
═════════════════════════════════════════════════════════════════════════════
4. CONTOH DATA SISWA YANG LOGIS (FORMAT SPREADSHEET)
────────────────────────────────────────────────────────────────────────────
Format Excel/CSV:
name | email | kelompok_asal | mtk | fisika | kimia | biologi | ekonomi | geografi | sosiologi | sejarah | minat | preferensi_studi | cita_cita | prestasi
CONTOH DATA IPA:
Rinto Wijaya|rinto@student.edu|IPA|92|88|75|70|0|0|0|0|Logika Komputer|Sains & Teknologi|Software Engineer|Juara 1 Coding
Siti Aminah|siti@student.edu|IPA|78|68|90|89|0|0|0|0|Farmasi & Kesehatan|Kesehatan & Ilmu Hayat|Apoteker|Olimpiade Biologi
Hendra Suryanto|hendra@student.edu|IPA|87|86|76|78|0|0|0|0|Inovasi Pertanian|Sains & Teknologi|Engineer Pertanian|Penemu Alat
CONTOH DATA IPS:
Rina Handayani|rina@student.edu|IPS|0|0|0|0|92|85|80|78|Bisnis & Kewirausahaan|Bisnis & Manajemen|Entrepreneur|Juara Kompetisi Bisnis
Lisa Maharani|lisa@student.edu|IPS|0|0|0|0|76|85|82|85|Pariwisata & Budaya|Sosial & Humaniora|Tour Guide|Pelatihan Tour Guide
═════════════════════════════════════════════════════════════════════════════
5. TIPS UNTUK NILAI YANG LOGIS
────────────────────────────────────────────────────────────────────────────
UNTUK TEKNOLOGI INFORMASI:
• MTK: 85-95 (prioritas tertinggi)
• Fisika: 75-90 (penting)
• Kimia, Biologi: 60-75 (tidak prioritas)
UNTUK TEKNIK:
• Fisika: 85-95 (prioritas tertinggi)
• MTK: 80-92 (penting)
• Kimia: 60-80 (cukup)
• Biologi: 50-70 (tidak prioritas)
UNTUK KESEHATAN:
• Biologi: 85-95 (prioritas tertinggi)
• Kimia: 80-92 (penting)
• MTK, Fisika: 65-80 (cukup)
UNTUK BISNIS (IPS):
• Ekonomi: 85-95 (prioritas tertinggi)
• MTK: 80-92 (penting untuk akuntansi)
• Geografi, Sosiologi: 70-85 (cukup)
• Sejarah: 60-80 (tidak prioritas)
UNTUK BAHASA & PARIWISATA (IPS):
• Sejarah: 85-95 (prioritas tertinggi - konteks budaya)
• Sosiologi: 80-92 (penting - interaksi sosial)
• Geografi: 75-90 (penting - destinasi wisata)
• Ekonomi: 65-85 (cukup - bisnis pariwisata)
• MTK: 50-75 (tidak prioritas)
═════════════════════════════════════════════════════════════════════════════
6. CARA IMPORT DATA KE SISTEM
────────────────────────────────────────────────────────────────────────────
Opsi 1: Manual via UI Admin
1. Login sebagai Admin/BK
2. Ke menu Admin > Guru BK > Tambah Siswa
3. Isi data satu per satu
4. Siswa bisa langsung akses sistem
Opsi 2: Bulk Import (melalui Database Langsung)
1. Siapkan file CSV dengan format di atas
2. Import ke database menggunakan migration/seeder
3. Hubungi admin untuk setup seeder khusus
Opsi 3: API Import (untuk integrasi sistem SMK)
1. Gunakan endpoint POST /api/students
2. Format JSON dengan data siswa
3. Sistem akan auto-generate rekomendasi
═════════════════════════════════════════════════════════════════════════════
7. TEMPLATE EXCEL UNTUK IMPORT (KOSONG SIAP DIISI)
────────────────────────────────────────────────────────────────────────────
Nama Siswa | Email | Kelompok | MTK | Fisika | Kimia | Biologi | Ekonomi | Geografi | Sosiologi | Sejarah | Minat | Preferensi | Cita-cita | Prestasi
─────────────────┼──────────────────────┼──────────┼─────┼────────┼───────┼─────────┼─────────┼──────────┼───────────┼─────────┼──────┼────────────┼───────────┼──────────
| | IPA/IPS | | | | | | | | | | | |
| | IPA/IPS | | | | | | | | | | | |
| | IPA/IPS | | | | | | | | | | | |
═════════════════════════════════════════════════════════════════════════════
8. CATATAN PENTING
────────────────────────────────────────────────────────────────────────────
✓ Pastikan nilai mapel sesuai dengan kelompok asal:
- IPA: isi MTK, Fisika, Kimia, Biologi (sisanya 0)
- IPS: isi Ekonomi, Geografi, Sosiologi, Sejarah (sisanya 0)
✓ Email harus unik (tidak boleh sama dengan siswa lain)
✓ Preferensi studi harus sesuai kategori yang tersedia (lihat poin 2)
✓ Minat dan Cita-cita akan membantu sistem memberikan rekomendasi yang lebih akurat
✓ Prestasi menunjukkan potensi siswa (bisa dilihat di chat history nantinya)
✓ Setelah data diimport, siswa bisa langsung login dengan:
- Email: sesuai yang didaftar
- Password: "password" (atau sesuai yang ditetapkan admin)
═════════════════════════════════════════════════════════════════════════════
Jika ada pertanyaan atau butuh bantuan import data, hubungi admin sistem!

View File

@ -0,0 +1,6 @@
Nama Alumni,NIS,Kelompok Asal,Tahun Masuk,MTK,Fisika,Kimia,Biologi,Ekonomi,Geografi,Sosiologi,Sejarah,Nilai Rata-Rata,Minat,Cita-Cita,Preferensi Studi,Prestasi,Jurusan Masuk,Ranking,Predicted Score,Tahun Lulus,IPK Lulus,Karir Outcome,Success Status,Catatan
Budi Santoso,12345678,IPA,2024,88,85,90,92,75,70,72,78,82.5,Coding dan AI,Software Engineer,Project Based,Juara LKS Provinsi 2024,Teknologi Informasi,1,89.3,2027,3.78,Backend Developer di PT Jago Software,Sangat Sukses,Rekomendasi akurat
Siti Rahmawati,87654321,IPS,2024,82,70,72,75,90,88,85,87,81.25,Bisnis dan Manajemen,Manajer Marketing,DuDi,Ketua OSIS dan Juara Debat,Manajemen Agribisnis,2,85.6,2027,3.55,Marketing Manager di PT Agro Trader,Sukses,Ranking 2 tapi sesuai preferensi
Ahmad Rifki,54321876,IPA,2024,90,88,89,87,76,71,73,79,83.25,Teknologi dan Robotika,Robotics Engineer,Praktik Langsung,Juara Kompetisi Robotika Nasional,Teknik,1,90.5,2027,3.82,Engineer di PT Elektronik Indonesia,Sangat Sukses,Sangat berbakat
Dewi Kusuma,11223344,IPS,2024,80,68,70,73,88,90,89,91,82.38,Pariwisata dan Komunikasi,Tour Operator,Blended Learning,Juara Public Speaking,Bahasa Komunikasi dan Pariwisata,3,80.2,2027,3.42,Coordinator di Bali Tourism,Sukses,Masih dalam pembelajaran
Rinto Wijaya,55667788,IPA,2024,85,82,88,90,74,69,71,77,80.75,Kesehatan dan Farmasi,Apoteker,Project Based,Aktif di PMR,Kesehatan,2,86.8,2027,3.65,Farmasi Staff di RS Permata,Sukses,Perpanjangan belajar
1 Nama Alumni NIS Kelompok Asal Tahun Masuk MTK Fisika Kimia Biologi Ekonomi Geografi Sosiologi Sejarah Nilai Rata-Rata Minat Cita-Cita Preferensi Studi Prestasi Jurusan Masuk Ranking Predicted Score Tahun Lulus IPK Lulus Karir Outcome Success Status Catatan
2 Budi Santoso 12345678 IPA 2024 88 85 90 92 75 70 72 78 82.5 Coding dan AI Software Engineer Project Based Juara LKS Provinsi 2024 Teknologi Informasi 1 89.3 2027 3.78 Backend Developer di PT Jago Software Sangat Sukses Rekomendasi akurat
3 Siti Rahmawati 87654321 IPS 2024 82 70 72 75 90 88 85 87 81.25 Bisnis dan Manajemen Manajer Marketing DuDi Ketua OSIS dan Juara Debat Manajemen Agribisnis 2 85.6 2027 3.55 Marketing Manager di PT Agro Trader Sukses Ranking 2 tapi sesuai preferensi
4 Ahmad Rifki 54321876 IPA 2024 90 88 89 87 76 71 73 79 83.25 Teknologi dan Robotika Robotics Engineer Praktik Langsung Juara Kompetisi Robotika Nasional Teknik 1 90.5 2027 3.82 Engineer di PT Elektronik Indonesia Sangat Sukses Sangat berbakat
5 Dewi Kusuma 11223344 IPS 2024 80 68 70 73 88 90 89 91 82.38 Pariwisata dan Komunikasi Tour Operator Blended Learning Juara Public Speaking Bahasa Komunikasi dan Pariwisata 3 80.2 2027 3.42 Coordinator di Bali Tourism Sukses Masih dalam pembelajaran
6 Rinto Wijaya 55667788 IPA 2024 85 82 88 90 74 69 71 77 80.75 Kesehatan dan Farmasi Apoteker Project Based Aktif di PMR Kesehatan 2 86.8 2027 3.65 Farmasi Staff di RS Permata Sukses Perpanjangan belajar

View File

@ -0,0 +1,6 @@
Nama Alumni,NIS,Kelompok Asal,MTK,Fisika,Kimia,Biologi,Ekonomi,Geografi,Sosiologi,Sejarah,Jurusan Masuk,Minat,Cita-Cita,Prestasi
Budi Santoso,,IPA,88,85,90,92,,,,,Teknologi Informasi,,Coding Engineer,
Siti Rahmawati,,IPS,82,,,,,90,88,85,Manajemen Agribisnis,,Manager,
Ahmad Rifki,,IPA,90,88,89,87,,,,,Teknik,,Engineer,
Dewi Kusuma,,IPS,80,,,,,88,90,89,Bahasa Komunikasi dan Pariwisata,,Tour Operator,
Rinto Wijaya,,IPA,85,82,88,90,,,,,Kesehatan,,Apoteker,
1 Nama Alumni NIS Kelompok Asal MTK Fisika Kimia Biologi Ekonomi Geografi Sosiologi Sejarah Jurusan Masuk Minat Cita-Cita Prestasi
2 Budi Santoso IPA 88 85 90 92 Teknologi Informasi Coding Engineer
3 Siti Rahmawati IPS 82 90 88 85 Manajemen Agribisnis Manager
4 Ahmad Rifki IPA 90 88 89 87 Teknik Engineer
5 Dewi Kusuma IPS 80 88 90 89 Bahasa Komunikasi dan Pariwisata Tour Operator
6 Rinto Wijaya IPA 85 82 88 90 Kesehatan Apoteker

View File

@ -0,0 +1,200 @@
# 📋 TEMPLATE IMPORT ALUMNI - SMA BIMA AMBULU
## Struktur File Excel yang Diperlukan
**File**: ALUMNI_BIMA_AMBULU_[TAHUN].xlsx
### Sheet 1: "Alumni Data"
| Kolom | Tipe | Contoh | Validasi | Wajib? |
|-------|------|--------|----------|--------|
| A | Nama Alumni | Budi Santoso | Text (max 255 char) | ✅ |
| B | NIS | 12345678 | Angka 8 digit | ✅ |
| C | Kelompok Asal | IPA | Dropdown: IPA / IPS | ✅ |
| D | Tahun Masuk | 2024 | Tahun (4 digit) | ✅ |
| E | MTK (Matematika) | 88 | 0-100 | ✅ |
| F | Fisika | 85 | 0-100 | ⭕ |
| G | Kimia | 90 | 0-100 | ⭕ |
| H | Biologi | 92 | 0-100 | ⭕ |
| I | Ekonomi | 75 | 0-100 | ⭕ |
| J | Geografi | 70 | 0-100 | ⭕ |
| K | Sosiologi | 72 | 0-100 | ⭕ |
| L | Sejarah | 78 | 0-100 | ⭕ |
| M | Nilai Rata-Rata | 82.5 | 0-100 | ⭕ |
| N | Minat | Teknologi, AI | Text | ⭕ |
| O | Cita-Cita | Software Engineer | Text | ⭕ |
| P | Preferensi Studi | Project Based | Dropdown | ⭕ |
| Q | Prestasi | Juara LKS Provinsi | Text | ⭕ |
| R | Jurusan Masuk | Teknologi Informasi | Dropdown (9 jurusan) | ✅ |
| S | Ranking (1-9) | 1 | 1-9 | ⭕ |
| T | Predicted Score | 89.3 | 0-100 | ⭕ |
| U | Tahun Lulus | 2027 | 4 digit | ⭕ |
| V | IPK Lulus | 3.78 | 0-4 | ⭕ |
| W | Karir Outcome | Backend Developer di PT XYZ | Text | ⭕ |
| X | Success Status | Sangat Sukses | Dropdown | ⭕ |
| Y | Catatan | Rekomendasi akurat | Text | ⭕ |
---
## Keterangan:
- ✅ = Wajib diisi
- ⭕ = Opsional tapi disarankan
- Untuk kolom nilai akademik: Isi sesuai jurusan SMA (IPA/IPS)
---
## Contoh Data Baris 1 (IPA):
```
Nama Alumni: Budi Santoso
NIS: 12345678
Kelompok Asal: IPA
Tahun Masuk: 2024
MTK: 88
Fisika: 85
Kimia: 90
Biologi: 92
Ekonomi: 75
Geografi: 70
Sosiologi: 72
Sejarah: 78
Nilai Rata-Rata: 82.5
Minat: Coding, AI, IoT
Cita-Cita: Menjadi Software Engineer
Preferensi Studi: Project Based
Prestasi: Juara LKS Tingkat Provinsi 2024
Jurusan Masuk: Teknologi Informasi
Ranking: 1
Predicted Score: 89.3
Tahun Lulus: 2027
IPK Lulus: 3.78
Karir Outcome: Bekerja di Jago Software sebagai Backend Developer
Success Status: Sangat Sukses
Catatan: Rekomendasi sangat akurat, IPK memuaskan
```
---
## Contoh Data Baris 2 (IPS):
```
Nama Alumni: Siti Rahmawati
NIS: 87654321
Kelompok Asal: IPS
Tahun Masuk: 2024
MTK: 82
Fisika: 70
Kimia: 72
Biologi: 75
Ekonomi: 90
Geografi: 88
Sosiologi: 85
Sejarah: 87
Nilai Rata-Rata: 81.25
Minat: Bisnis, Manajemen
Cita-Cita: Jadi Manajer Marketing
Preferensi Studi: DuDi
Prestasi: Ketua OSIS, Juara Debat
Jurusan Masuk: Manajemen Agribisnis
Ranking: 2
Predicted Score: 85.6
Tahun Lulus: 2027
IPK Lulus: 3.55
Karir Outcome: Kerja di Perusahaan Agro Trader
Success Status: Sukses
Catatan: Ranking 2 tapi sesuai preferensi
```
---
## Format File:
**File Format**: `.xlsx` (Microsoft Excel 2007+)
**Encoding**: UTF-8
**Delimiter**: N/A (Excel native format)
**Sheet Name**: "Alumni Data"
**Header Row**: Baris 1 (nama kolom)
**Data Rows**: Mulai baris 2
---
## Panduan Pengisian Per Kelompok:
### Kelompok IPA - Prioritas Nilai:
1. **MTK** - Wajib (paling penting)
2. Fisika
3. Kimia
4. Biologi
5. (Ekonomi, Geografi, Sosiologi, Sejarah - opsional)
### Kelompok IPS - Prioritas Nilai:
1. **MTK** - Wajib
2. Ekonomi
3. Geografi
4. Sosiologi
5. Sejarah
6. (Fisika, Kimia, Biologi - opsional)
---
## Dropdown Options (STANDARDIZED):
### Kelompok Asal:
- IPA
- IPS
### Preferensi Studi:
- Praktik Langsung
- DuDi
- Project Based
- Blended Learning
### Jurusan Masuk:
1. Teknologi Informasi
2. Teknik
3. Kesehatan
4. Bisnis
5. Peternakan
6. Produksi Pertanian
7. Teknologi Pertanian
8. Manajemen Agribisnis
9. Bahasa, Komunikasi, dan Pariwisata
### Success Status:
- Sangat Sukses
- Sukses
- Cukup
- Kurang Sukses
---
## ⚠️ Validasi Checklist:
Sebelum submit file, pastikan:
- [ ] Semua baris memiliki Nama Alumni
- [ ] Semua NIS tidak duplikat
- [ ] Nilai akademik dalam range 0-100
- [ ] Tidak ada baris kosong di tengah
- [ ] Format tahun: 4 digit (2024, bukan 24)
- [ ] Format IPK: desimal 0-4 (3.78, bukan 378)
- [ ] Kelompok Asal hanya IPA atau IPS
- [ ] Jurusan Masuk sesuai dengan 9 jurusan Polije
- [ ] Preferensi Studi sesuai pilihan dropdown (jika ada)
- [ ] Success Status sesuai pilihan (jika ada)
- [ ] Tidak ada karakter spesial di nama (hanya alfanumerik, spasi, tanda hubung)
---
## 📧 Cara Submit:
1. **Isi file Excel** sesuai template
2. **Simpan sebagai**: `ALUMNI_BIMA_AMBULU_[TAHUN].xlsx`
3. **Kirim ke**: Admin Polije
4. **Atau upload melalui**: Admin Panel > Alumni Import
---
## 📞 Support:
Jika ada pertanyaan tentang format atau kolom, hubungi administrator sistem.

View File

@ -47,6 +47,30 @@ public function dashboard()
->take(5)
->get();
// Data untuk chart - semua jurusan
$allMajorsChart = Recommendation::selectRaw("
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
COUNT(*) as count
")
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
->orderBy('count', 'desc')
->get();
// Persiapkan data untuk Chart.js
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
// Top majors untuk horizontal bar chart
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$topMajorsCounts = $topMajors->pluck('count')->toArray();
return view('admin.dashboard', compact(
'totalSiswa',
'totalRekomendasi',
@ -55,7 +79,13 @@ public function dashboard()
'recentStudents',
'recentRecommendations',
'kelompokStats',
'topMajors'
'topMajors',
'chartMajorNames',
'chartMajorCounts',
'chartKelompokNames',
'chartKelompokCounts',
'topMajorsChart',
'topMajorsCounts'
));
}
@ -124,7 +154,7 @@ public function jurusanCreate()
public function jurusanStore(Request $request)
{
$request->validate([
'nama_jurusan' => 'required|string|max:255|unique:polije_majors,nama_jurusan',
'nama_jurusan' => 'required|string|max:255|unique:jurusan_polije,nama_jurusan',
'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string',
@ -162,7 +192,7 @@ public function jurusanUpdate(Request $request, $id)
$jurusan = PolijeMajor::findOrFail($id);
$request->validate([
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('polije_majors', 'nama_jurusan')->ignore($jurusan->id)],
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('jurusan_polije', 'nama_jurusan')->ignore($jurusan->id)],
'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string',
@ -336,8 +366,8 @@ public function riwayatChatbot(Request $request)
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('prompt', 'like', "%{$search}%")
->orWhere('response', 'like', "%{$search}%")
$q->where('pertanyaan', 'like', "%{$search}%")
->orWhere('jawaban', 'like', "%{$search}%")
->orWhereHas('user', function ($q2) use ($search) {
$q2->where('name', 'like', "%{$search}%");
});

View File

@ -47,6 +47,30 @@ public function dashboard()
->take(5)
->get();
// Data untuk chart - semua jurusan
$allMajorsChart = Recommendation::selectRaw("
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
COUNT(*) as count
")
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
->orderBy('count', 'desc')
->get();
// Persiapkan data untuk Chart.js
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
// Top majors untuk horizontal bar chart
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
return trim($name, '"');
})->toArray();
$topMajorsCounts = $topMajors->pluck('count')->toArray();
return view('bk.dashboard', compact(
'totalSiswa',
'totalRekomendasi',
@ -55,7 +79,13 @@ public function dashboard()
'recentStudents',
'recentRecommendations',
'kelompokStats',
'topMajors'
'topMajors',
'chartMajorNames',
'chartMajorCounts',
'chartKelompokNames',
'chartKelompokCounts',
'topMajorsChart',
'topMajorsCounts'
));
}
@ -148,8 +178,8 @@ public function riwayatChatbot(Request $request)
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('prompt', 'like', "%{$search}%")
->orWhere('response', 'like', "%{$search}%")
$q->where('pertanyaan', 'like', "%{$search}%")
->orWhere('jawaban', 'like', "%{$search}%")
->orWhereHas('user', function ($q2) use ($search) {
$q2->where('name', 'like', "%{$search}%");
});
@ -181,7 +211,7 @@ public function jurusanCreate()
public function jurusanStore(Request $request)
{
$request->validate([
'nama_jurusan' => 'required|string|max:255|unique:polije_majors,nama_jurusan',
'nama_jurusan' => 'required|string|max:255|unique:jurusan_polije,nama_jurusan',
'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string',
@ -219,7 +249,7 @@ public function jurusanUpdate(Request $request, $id)
$jurusan = PolijeMajor::findOrFail($id);
$request->validate([
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('polije_majors', 'nama_jurusan')->ignore($jurusan->id)],
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('jurusan_polije', 'nama_jurusan')->ignore($jurusan->id)],
'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string',

View File

@ -36,7 +36,7 @@ public function index(Request $request)
if ($sessionId) {
// Lanjutkan sesi lama — ambil semua chat dari sesi ini
$chats = ChatHistory::where('user_id', $user->id)
->where('session_id', $sessionId)
->where('id_sesi', $sessionId)
->orderBy('created_at', 'asc')
->get();
@ -140,14 +140,32 @@ public function send(Request $request)
// Panggil Gemini API dengan conversation history
$response = $this->geminiService->chat($message, $context, $chatHistory);
// Normalisasi respons agar error tetap memiliki pesan yang konsisten.
$isSuccess = (bool) ($response['success'] ?? false);
$errorCode = (string) ($response['error_code'] ?? 'CHAT_SERVICE_ERROR');
$responseMessage = trim((string) ($response['message'] ?? ''));
if ($responseMessage === '') {
$responseMessage = $isSuccess
? 'Respons berhasil diproses.'
: 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.';
}
if (!$isSuccess) {
$responseMessage = "[ERROR:{$errorCode}] {$responseMessage}";
}
$response['message'] = $responseMessage;
$response['error_code'] = $isSuccess ? null : $errorCode;
// Simpan chat ke database dengan session_id dan recommendation_id
if ($user && isset($response['message'])) {
if ($user) {
ChatHistory::create([
'user_id' => $user->id,
'session_id' => $sessionId,
'recommendation_id' => $recommendationId,
'prompt' => $message,
'response' => $response['message'],
'id_sesi' => $sessionId,
'id_rekomendasi' => $recommendationId,
'pertanyaan' => $message,
'jawaban' => $responseMessage,
]);
}
@ -167,8 +185,8 @@ public function historyChat()
->orderBy('created_at', 'desc')
->get();
// Kelompokkan per session_id
$sessions = $chatHistories->groupBy('session_id')->map(function ($chats, $sessionId) {
// Kelompokkan per id_sesi
$sessions = $chatHistories->groupBy('id_sesi')->map(function ($chats, $sessionId) {
$first = $chats->last(); // oldest in group (karena desc)
$last = $chats->first(); // newest in group
$rec = $first->recommendation;
@ -287,12 +305,12 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
}
// Cari chat history yang mengandung kata kunci serupa
$query = ChatHistory::select('prompt', 'response', 'created_at')
$query = ChatHistory::select('pertanyaan', 'jawaban', 'created_at')
->where('user_id', $currentUserId);
$query->where(function ($q) use ($keywords) {
foreach ($keywords as $keyword) {
$q->orWhere('prompt', 'like', "%{$keyword}%");
$q->orWhere('pertanyaan', 'like', "%{$keyword}%");
}
});
@ -307,7 +325,7 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
// Scoring: hitung berapa keyword yang cocok
$scored = [];
foreach ($candidates as $chat) {
$promptLower = strtolower($chat->prompt);
$promptLower = strtolower($chat->pertanyaan);
$matchCount = 0;
foreach ($keywords as $kw) {
if (stripos($promptLower, $kw) !== false) {
@ -317,8 +335,8 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
$ratio = $matchCount / count($keywords);
if ($ratio >= 0.4) { // minimal 40% keyword cocok
$scored[] = [
'prompt' => $chat->prompt,
'response' => Str::limit($chat->response, 300),
'prompt' => $chat->pertanyaan,
'response' => Str::limit($chat->jawaban, 300),
'score' => $ratio,
];
}

View File

@ -9,16 +9,56 @@ class ChatHistory extends Model
{
use HasFactory;
protected $table = 'chat_histories';
protected $table = 'riwayat_chat';
protected $fillable = [
'user_id',
'session_id',
'recommendation_id',
'prompt',
'response',
'id_sesi',
'id_rekomendasi',
'pertanyaan',
'jawaban',
];
public function getSessionIdAttribute()
{
return $this->attributes['id_sesi'] ?? null;
}
public function setSessionIdAttribute($value): void
{
$this->attributes['id_sesi'] = $value;
}
public function getPromptAttribute()
{
return $this->attributes['pertanyaan'] ?? null;
}
public function setPromptAttribute($value): void
{
$this->attributes['pertanyaan'] = $value;
}
public function getResponseAttribute()
{
return $this->attributes['jawaban'] ?? null;
}
public function setResponseAttribute($value): void
{
$this->attributes['jawaban'] = $value;
}
public function getRecommendationIdAttribute()
{
return $this->attributes['id_rekomendasi'] ?? null;
}
public function setRecommendationIdAttribute($value): void
{
$this->attributes['id_rekomendasi'] = $value;
}
public function user()
{
return $this->belongsTo(User::class);
@ -26,6 +66,6 @@ public function user()
public function recommendation()
{
return $this->belongsTo(Recommendation::class);
return $this->belongsTo(Recommendation::class, 'id_rekomendasi');
}
}

View File

@ -9,6 +9,8 @@ class PolijeMajor extends Model
{
use HasFactory;
protected $table = 'jurusan_polije';
protected $fillable = [
'nama_jurusan',
'deskripsi',

View File

@ -9,6 +9,8 @@ class Recommendation extends Model
{
use HasFactory;
protected $table = 'rekomendasi';
protected $fillable = [
'user_id',
'mtk',

View File

@ -33,7 +33,7 @@ public function chat($message, $context = [], $chatHistory = [])
if (empty($this->apiKey)) {
return [
'success' => false,
'message' => 'API Key tidak tersedia. Silakan konfigurasi GEMINI_API_KEY di .env'
'message' => 'Layanan chatbot belum siap digunakan. Silakan hubungi pengelola sistem.'
];
}
@ -83,53 +83,27 @@ public function chat($message, $context = [], $chatHistory = [])
]
];
// Jika backend Python dikonfigurasi, gunakan sebagai gateway Gemini terlebih dahulu.
if (!empty($this->backendUrl)) {
$proxyResponse = $this->sendViaPythonBackend($payload);
if (($proxyResponse['success'] ?? false) === true) {
return $proxyResponse;
}
Log::warning('Python Gemini backend failed, fallback to direct API', [
'error' => $proxyResponse['message'] ?? 'unknown',
]);
// Mode Python-only: chatbot wajib melalui backend Python.
if (empty($this->backendUrl)) {
return [
'success' => false,
'message' => 'Layanan chatbot belum siap digunakan saat ini. Silakan coba kembali beberapa saat lagi.',
];
}
// Try each model until one works
foreach ($this->models as $model) {
$url = $this->baseUrl . $model . ':generateContent?key=' . $this->apiKey;
Log::info('Trying Gemini model', ['model' => $model]);
$response = Http::timeout(30)
->withHeaders(['Content-Type' => 'application/json'])
->post($url, $payload);
if ($response->successful()) {
$data = $response->json();
if (isset($data['candidates'][0]['content']['parts'][0]['text'])) {
Log::info('Gemini API success', ['model' => $model]);
return [
'success' => true,
'message' => $data['candidates'][0]['content']['parts'][0]['text']
];
}
}
// If 429 (rate limit) or 404 (model not found), try next model
$status = $response->status();
Log::warning("Gemini model {$model} failed", ['status' => $status]);
if ($status === 429) {
// Wait briefly before trying next model
sleep(1);
}
$proxyResponse = $this->sendViaPythonBackend($payload);
if (($proxyResponse['success'] ?? false) === true) {
return $proxyResponse;
}
// All models failed
Log::error('All Gemini models failed, using fallback');
return $this->getFallbackResponse($message, $context, $chatHistory);
Log::error('Python Gemini backend failed in Python-only mode', [
'error' => $proxyResponse['message'] ?? 'unknown',
]);
return [
'success' => false,
'message' => $proxyResponse['message'] ?? 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba lagi.',
];
} catch (\Exception $e) {
Log::error('Gemini Service Exception', [
@ -138,7 +112,10 @@ public function chat($message, $context = [], $chatHistory = [])
'line' => $e->getLine()
]);
return $this->getFallbackResponse($message, $context, $chatHistory);
return [
'success' => false,
'message' => 'Maaf, layanan chatbot sedang mengalami kendala. Silakan coba kembali beberapa saat lagi.',
];
}
}
@ -161,9 +138,19 @@ protected function sendViaPythonBackend(array $payload): array
]);
if (!$response->successful()) {
$status = $response->status();
$mappedMessage = match ($status) {
401, 403 => 'Maaf, layanan chatbot sedang dibatasi sementara. Silakan coba kembali nanti.',
422 => 'Pesan belum dapat diproses. Silakan perjelas pertanyaan Anda dan coba lagi.',
429 => 'Layanan chatbot sedang ramai digunakan. Silakan tunggu sebentar lalu coba lagi.',
500, 502, 503, 504 => 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.',
default => 'Layanan chatbot sedang tidak tersedia. Silakan coba kembali nanti.',
};
return [
'success' => false,
'message' => 'Python backend error HTTP ' . $response->status(),
'message' => $mappedMessage,
];
}
@ -177,7 +164,7 @@ protected function sendViaPythonBackend(array $payload): array
return [
'success' => false,
'message' => $data['message'] ?? 'Python backend response invalid',
'message' => $data['message'] ?? 'Maaf, layanan chatbot belum dapat memberikan jawaban saat ini.',
];
} catch (\Exception $e) {
Log::warning('Python backend exception', [
@ -186,7 +173,7 @@ protected function sendViaPythonBackend(array $payload): array
return [
'success' => false,
'message' => 'Python backend exception',
'message' => 'Koneksi layanan chatbot sedang bermasalah. Silakan coba kembali beberapa saat lagi.',
];
}
}

View File

@ -0,0 +1,86 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('recommendations') && !Schema::hasTable('rekomendasi')) {
Schema::rename('recommendations', 'rekomendasi');
}
if (Schema::hasTable('polije_majors') && !Schema::hasTable('jurusan_polije')) {
Schema::rename('polije_majors', 'jurusan_polije');
}
if (Schema::hasTable('chat_histories') && !Schema::hasTable('riwayat_chat')) {
Schema::rename('chat_histories', 'riwayat_chat');
}
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'session_id') && !Schema::hasColumn('riwayat_chat', 'id_sesi')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('session_id', 'id_sesi');
});
}
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'prompt') && !Schema::hasColumn('riwayat_chat', 'pertanyaan')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('prompt', 'pertanyaan');
});
}
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'response') && !Schema::hasColumn('riwayat_chat', 'jawaban')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('response', 'jawaban');
});
}
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'recommendation_id') && !Schema::hasColumn('riwayat_chat', 'id_rekomendasi')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('recommendation_id', 'id_rekomendasi');
});
}
}
public function down(): void
{
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'id_sesi') && !Schema::hasColumn('riwayat_chat', 'session_id')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('id_sesi', 'session_id');
});
}
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'pertanyaan') && !Schema::hasColumn('riwayat_chat', 'prompt')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('pertanyaan', 'prompt');
});
}
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'jawaban') && !Schema::hasColumn('riwayat_chat', 'response')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('jawaban', 'response');
});
}
if (Schema::hasTable('riwayat_chat') && Schema::hasColumn('riwayat_chat', 'id_rekomendasi') && !Schema::hasColumn('riwayat_chat', 'recommendation_id')) {
Schema::table('riwayat_chat', function (Blueprint $table) {
$table->renameColumn('id_rekomendasi', 'recommendation_id');
});
}
if (Schema::hasTable('riwayat_chat') && !Schema::hasTable('chat_histories')) {
Schema::rename('riwayat_chat', 'chat_histories');
}
if (Schema::hasTable('jurusan_polije') && !Schema::hasTable('polije_majors')) {
Schema::rename('jurusan_polije', 'polije_majors');
}
if (Schema::hasTable('rekomendasi') && !Schema::hasTable('recommendations')) {
Schema::rename('rekomendasi', 'recommendations');
}
}
};

View File

@ -0,0 +1,223 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Recommendation;
use App\Models\PolijeMajor;
use Illuminate\Database\Seeder;
class BimaAmbulustudentSeeder extends Seeder
{
/**
* SEEDER UNTUK DATA SISWA SMK BIMA AMBULU
*
* Edit data siswa di bawah sesuai dengan data siswa dari SMK Bima Ambulu
* Format:
* - Kelompok: IPA atau IPS
* - Nilai: 0-100
* - Email: harus unik
*
* Run: php artisan db:seed --class=BimaAmbulustudentSeeder
*/
public function run(): void
{
$studentsData = [
// ========== SISWA SMK BIMA AMBULU - KELOMPOK IPA ==========
[
'name' => 'Akmal Fiqri',
'email' => 'akmal.fiqri@bima.student',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 85, 'fisika' => 82, 'kimia' => 75, 'biologi' => 70],
'minat' => 'Teknologi & Inovasi',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Software Engineer',
'prestasi' => 'Aktif di klub robotika',
],
[
'name' => 'Sasha Putri Aulia',
'email' => 'sasha.putri@bima.student',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 78, 'fisika' => 80, 'kimia' => 85, 'biologi' => 82],
'minat' => 'Kesehatan & Biologi',
'pref_studi' => 'Kesehatan & Ilmu Hayat',
'cita_cita' => 'Dokter Umum',
'prestasi' => 'Juara Olimpiade Biologi',
],
[
'name' => 'Budi Kusuma',
'email' => 'budi.kusuma@bima.student',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 82, 'fisika' => 88, 'kimia' => 76, 'biologi' => 68],
'minat' => 'Teknik & Mesin',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Insinyur Mesin',
'prestasi' => 'Peserta Kompetisi Teknik',
],
[
'name' => 'Dina Hartini',
'email' => 'dina.hartini@bima.student',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 75, 'fisika' => 72, 'kimia' => 88, 'biologi' => 90],
'minat' => 'Farmasi & Kimia',
'pref_studi' => 'Kesehatan & Ilmu Hayat',
'cita_cita' => 'Apoteker',
'prestasi' => 'Ranking 5 Besar',
],
[
'name' => 'Eka Prasetyo',
'email' => 'eka.prasetyo@bima.student',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 88, 'fisika' => 85, 'kimia' => 72, 'biologi' => 75],
'minat' => 'Pemrograman & Coding',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Web Developer',
'prestasi' => 'Peserta Hackathon',
],
// ========== SISWA SMK BIMA AMBULU - KELOMPOK IPS ==========
[
'name' => 'Fahmi Rizki',
'email' => 'fahmi.rizki@bima.student',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 88, 'geografi' => 80, 'sosiologi' => 78, 'sejarah' => 75],
'minat' => 'Bisnis & Keuangan',
'pref_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Akuntan Profesional',
'prestasi' => 'Aktif di organisasi bisnis',
],
[
'name' => 'Gina Melani',
'email' => 'gina.melani@bima.student',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 85, 'geografi' => 88, 'sosiologi' => 82, 'sejarah' => 80],
'minat' => 'Pariwisata & Budaya',
'pref_studi' => 'Sosial & Humaniora',
'cita_cita' => 'Tour Guide Profesional',
'prestasi' => 'Pelatihan Pariwisata',
],
[
'name' => 'Hasan Wijaya',
'email' => 'hasan.wijaya@bima.student',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 82, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78],
'minat' => 'Manajemen & Administrasi',
'pref_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Manager Perusahaan',
'prestasi' => 'Peserta Case Study',
],
[
'name' => 'Irma Santika',
'email' => 'irma.santika@bima.student',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 75, 'geografi' => 92, 'sosiologi' => 85, 'sejarah' => 88],
'minat' => 'Budaya & Komunikasi',
'pref_studi' => 'Sosial & Humaniora',
'cita_cita' => 'Jurnalis',
'prestasi' => 'Penulis artikel',
],
[
'name' => 'Joko Supriyanto',
'email' => 'joko.supriyanto@bima.student',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 80, 'geografi' => 75, 'sosiologi' => 88, 'sejarah' => 82],
'minat' => 'Sosial & Masyarakat',
'pref_studi' => 'Sosial & Humaniora',
'cita_cita' => 'Pekerja Sosial',
'prestasi' => 'Aktif di kegiatan sosial',
],
];
$majorNames = PolijeMajor::pluck('nama_jurusan')->toArray();
foreach ($studentsData as $data) {
// Cek apakah siswa sudah ada
$existingUser = User::where('email', $data['email'])->first();
if ($existingUser) {
$this->command->warn("⚠ Siswa {$data['name']} ({$data['email']}) sudah ada di sistem, skip...");
continue;
}
// Buat user siswa baru
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt('password'), // Password default
'role' => 'siswa',
'kelompok_asal' => $data['kelompok_asal'],
]);
// Hitung skor rekomendasi berdasarkan bobot mapel
$scores = $this->calculateScores($data, $majorNames);
// Ambil top 3 dengan skor tertinggi
arsort($scores);
$topRecommendations = array_slice($scores, 0, 3, true);
// Buat data rekomendasi dengan ranking
$recommendations = [];
foreach ($topRecommendations as $majorName => $score) {
$recommendations[] = [
'jurusan' => $majorName,
'skor' => round($score, 2),
'detail' => "Rekomendasi berdasarkan nilai, minat, dan preferensi studi siswa.",
];
}
// Simpan rekomendasi ke database
Recommendation::create([
'user_id' => $user->id,
'mtk' => $data['values']['mtk'] ?? 0,
'fisika' => $data['values']['fisika'] ?? 0,
'kimia' => $data['values']['kimia'] ?? 0,
'biologi' => $data['values']['biologi'] ?? 0,
'ekonomi' => $data['values']['ekonomi'] ?? 0,
'sejarah' => $data['values']['sejarah'] ?? 0,
'geografi' => $data['values']['geografi'] ?? 0,
'sosiologi' => $data['values']['sosiologi'] ?? 0,
'minat' => $data['minat'],
'preferensi_studi' => $data['pref_studi'],
'cita_cita' => $data['cita_cita'],
'prestasi' => $data['prestasi'],
'hasil_rekomendasi' => json_encode($recommendations),
]);
$this->command->info("✅ Siswa {$data['name']} ({$data['email']}) ditambahkan dengan rekomendasi jurusan.");
}
$this->command->info("\n✅ Impor data siswa SMK Bima Ambulu selesai!");
}
private function calculateScores(array $studentData, array $majorNames): array
{
$scores = [];
$majors = PolijeMajor::all()->keyBy('nama_jurusan');
foreach ($majorNames as $majorName) {
if (!isset($majors[$majorName])) continue;
$major = $majors[$majorName];
$bobot = $major->bobot_mapel;
$score = 0;
// Hitung skor berdasarkan bobot dan nilai siswa
foreach ($bobot as $subject => $weight) {
$value = $studentData['values'][$subject] ?? 0;
$score += ($value * $weight) / 100;
}
// Bonus untuk preferensi studi yang sesuai
if (in_array($studentData['pref_studi'], $major->preferensi_studi)) {
$score += 5;
}
$scores[$majorName] = $score;
}
return $scores;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Recommendation;
use App\Models\PolijeMajor;
use Illuminate\Database\Seeder;
class RegenerateRecommendationsSeeder extends Seeder
{
public function run(): void
{
// Cari siswa yang belum punya rekomendasi atau rekomendasi mereka kosong
$studentsWithoutRecs = User::where('role', 'siswa')
->whereNotIn('id', Recommendation::pluck('user_id')->toArray())
->get();
$majorNames = PolijeMajor::pluck('nama_jurusan')->toArray();
$count = 0;
foreach ($studentsWithoutRecs as $student) {
// Jika siswa belum punya rekomendasi sama sekali, generate random recommendation
if (!$student->hasRecommendation) {
$scores = [];
// Generate random scores untuk setiap jurusan
foreach ($majorNames as $majorName) {
$major = PolijeMajor::where('nama_jurusan', $majorName)->first();
if ($major) {
$bobot = $major->bobot_mapel;
$score = 0;
// Random scoring (untuk demo)
foreach ($bobot as $subject => $weight) {
$randomValue = rand(70, 95);
$score += ($randomValue * $weight) / 100;
}
// Random bonus untuk preferensi
if (rand(0, 1) == 1) {
$score += 5;
}
$scores[$majorName] = $score;
}
}
arsort($scores);
$topRecommendations = array_slice($scores, 0, 3, true);
$recommendations = [];
foreach ($topRecommendations as $majorName => $score) {
$recommendations[] = [
'jurusan' => $majorName,
'skor' => round($score, 2),
'detail' => "Rekomendasi berdasarkan nilai dan minat siswa.",
];
}
Recommendation::create([
'user_id' => $student->id,
'mtk' => rand(60, 95),
'fisika' => rand(60, 95),
'kimia' => rand(60, 95),
'biologi' => rand(60, 95),
'ekonomi' => rand(60, 95),
'geografi' => rand(60, 95),
'sosiologi' => rand(60, 95),
'sejarah' => rand(60, 95),
'minat' => 'Umum',
'preferensi_studi' => 'Sains & Teknologi',
'cita_cita' => 'Profesional',
'prestasi' => 'Aktif',
'hasil_rekomendasi' => $recommendations, // Pass array langsung, bukan json_encode
]);
$count++;
}
}
$this->command->info("✅ Regenerasi rekomendasi untuk {$count} siswa selesai!");
}
}

View File

@ -0,0 +1,286 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Recommendation;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class StudentRecommendationSeeder extends Seeder
{
/**
* Run the database seeds.
* Data siswa dan rekomendasi dengan logika yang akurat
*/
public function run(): void
{
// Majors data mapping untuk rekomendasi
$majorsWeights = [
'teknologi_informasi' => ['mtk' => 0.30, 'fisika' => 0.25, 'kimia' => 0.15, 'biologi' => 0.05],
'teknik' => ['mtk' => 0.25, 'fisika' => 0.35, 'kimia' => 0.20, 'biologi' => 0.05],
'produksi_pertanian' => ['mtk' => 0.15, 'biologi' => 0.35, 'kimia' => 0.25, 'fisika' => 0.10],
'peternakan' => ['biologi' => 0.40, 'kimia' => 0.20, 'mtk' => 0.15, 'fisika' => 0.05],
'manajemen_agribisnis' => ['ekonomi' => 0.35, 'geografi' => 0.25, 'mtk' => 0.20, 'sosiologi' => 0.15],
'akuntansi' => ['ekonomi' => 0.40, 'mtk' => 0.25, 'sejarah' => 0.15, 'sosiologi' => 0.10],
'bahasa_komunikasi' => ['sosiologi' => 0.30, 'sejarah' => 0.25, 'ekonomi' => 0.20, 'geografi' => 0.15],
];
$studentData = [
// IPA Students
[
'name' => 'Adi Pratama',
'nis' => '001',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 92, 'fisika' => 88, 'kimia' => 85, 'biologi' => 78],
'minat' => 'Logika Komputer',
'preferensi_studi' => 'Sains & Teknologi',
'cita_cita' => 'Software Engineer',
'prestasi' => 'Juara 2 Olimpiade Informatika',
'expected_major' => 'Teknologi Informasi',
],
[
'name' => 'Bella Maharani',
'nis' => '002',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 85, 'fisika' => 90, 'kimia' => 88, 'biologi' => 80],
'minat' => 'Mesin & Otomasi',
'preferensi_studi' => 'Sains & Teknologi',
'cita_cita' => 'Insinyur Mesin',
'prestasi' => 'Juara Lomba Robotika Regional',
'expected_major' => 'Teknik',
],
[
'name' => 'Citra Dewi',
'nis' => '003',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 78, 'fisika' => 75, 'kimia' => 88, 'biologi' => 92],
'minat' => 'Alam Tanaman',
'preferensi_studi' => 'Pertanian & Lingkungan',
'cita_cita' => 'Agronomis',
'prestasi' => 'Juara Pameran Tanaman Hidroponik',
'expected_major' => 'Produksi Pertanian',
],
[
'name' => 'Doni Kusuma',
'nis' => '004',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 82, 'fisika' => 80, 'kimia' => 92, 'biologi' => 88],
'minat' => 'Kimia & Biologi',
'preferensi_studi' => 'Kesehatan & Ilmu Hayat',
'cita_cita' => 'Peneliti Biologi',
'prestasi' => 'Penulis Jurnal Ilmiah Tingkat Sekolah',
'expected_major' => 'Peternakan',
],
// IPS Students
[
'name' => 'Eka Prasetyo',
'nis' => '005',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 90, 'geografi' => 87, 'sosiologi' => 85, 'sejarah' => 80],
'minat' => 'Bisnis',
'preferensi_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Entrepreneur Muda',
'prestasi' => 'Juara Kompetisi Bisnis Plan Nasional',
'expected_major' => 'Manajemen Agribisnis',
],
[
'name' => 'Fitri Handayani',
'nis' => '006',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 88, 'mtk' => 85, 'sejarah' => 82, 'sosiologi' => 80],
'minat' => 'Akuntansi',
'preferensi_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Akuntan Profesional',
'prestasi' => 'Finalis Kompetisi Akuntansi Wilayah',
'expected_major' => 'Akuntansi',
],
[
'name' => 'Gita Salsabila',
'nis' => '007',
'kelompok_asal' => 'IPS',
'values' => ['sosiologi' => 92, 'sejarah' => 88, 'ekonomi' => 80, 'geografi' => 85],
'minat' => 'Komunikasi Sosial',
'preferensi_studi' => 'Seni & Komunikasi',
'cita_cita' => 'Presenter Berita',
'prestasi' => 'Juara Debat Tingkat Propinsi',
'expected_major' => 'Bahasa Komunikasi',
],
[
'name' => 'Hendra Wijaya',
'nis' => '008',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 85, 'geografi' => 88, 'mtk' => 80, 'sosiologi' => 82],
'minat' => 'Bisnis',
'preferensi_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Manajer Bisnis',
'prestasi' => 'Pengusaha Kuliner Muda',
'expected_major' => 'Manajemen Agribisnis',
],
// More diverse students
[
'name' => 'Ibu Musim',
'nis' => '009',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 88, 'fisika' => 85, 'kimia' => 86, 'biologi' => 84],
'minat' => 'Teknologi & Inovasi',
'preferensi_studi' => 'Sains & Teknologi',
'cita_cita' => 'Product Manager Tech',
'prestasi' => 'Top 10 Innovation Challenge',
'expected_major' => 'Teknologi Informasi',
],
[
'name' => 'Joko Santoso',
'nis' => '010',
'kelompok_asal' => 'IPA',
'values' => ['fisika' => 92, 'mtk' => 90, 'kimia' => 84, 'biologi' => 76],
'minat' => 'Listrik & Energi',
'preferensi_studi' => 'Sains & Teknologi',
'cita_cita' => 'Insinyur Elektro',
'prestasi' => 'Juara Kompetisi Energi Terbarukan',
'expected_major' => 'Teknik',
],
];
foreach ($studentData as $data) {
// Create student user
$user = User::create([
'name' => $data['name'],
'email' => strtolower(str_replace(' ', '.', $data['name'])) . '@student.polije.ac.id',
'password' => Hash::make('password123'),
'role' => 'siswa',
'nis' => $data['nis'],
'kelompok_asal' => $data['kelompok_asal'],
'foto' => null,
]);
// Create recommendation with logical scoring
$rekomendasiHasil = $this->generateRecommendationResult(
$data['values'],
$data['preferensi_studi'],
$data['expected_major'],
$majorsWeights
);
$recommendation = Recommendation::create([
'user_id' => $user->id,
'mtk' => $data['values']['mtk'] ?? null,
'fisika' => $data['values']['fisika'] ?? null,
'kimia' => $data['values']['kimia'] ?? null,
'biologi' => $data['values']['biologi'] ?? null,
'ekonomi' => $data['values']['ekonomi'] ?? null,
'geografi' => $data['values']['geografi'] ?? null,
'sosiologi' => $data['values']['sosiologi'] ?? null,
'sejarah' => $data['values']['sejarah'] ?? null,
'minat' => $data['minat'],
'preferensi_studi' => $data['preferensi_studi'],
'cita_cita' => $data['cita_cita'],
'prestasi' => $data['prestasi'],
'hasil_rekomendasi' => $rekomendasiHasil,
]);
// Create sample chat history
$sessionId = Str::uuid()->toString();
$this->createSampleChatHistory($user->id, $recommendation->id, $sessionId, $data);
}
$this->command->info('Student dan Recommendation data berhasil diisi dengan ' . count($studentData) . ' siswa!');
}
/**
* Generate logical recommendation result based on values
*/
private function generateRecommendationResult($values, $preferensi, $expectedMajor, $majorsWeights)
{
$majors = [
[
'jurusan' => 'Teknologi Informasi',
'skor' => $this->calculateScore($values, ['mtk' => 0.30, 'fisika' => 0.25, 'kimia' => 0.15, 'biologi' => 0.05]),
'detail' => 'Cocok untuk minat teknologi dan programming.'
],
[
'jurusan' => 'Teknik',
'skor' => $this->calculateScore($values, ['mtk' => 0.25, 'fisika' => 0.35, 'kimia' => 0.20, 'biologi' => 0.05]),
'detail' => 'Sesuai dengan kemampuan fisika dan matematika tinggi.'
],
[
'jurusan' => 'Produksi Pertanian',
'skor' => $this->calculateScore($values, ['mtk' => 0.15, 'biologi' => 0.35, 'kimia' => 0.25, 'fisika' => 0.10]),
'detail' => 'Cocok untuk minat di bidang pertanian dan lingkungan.'
],
[
'jurusan' => 'Peternakan',
'skor' => $this->calculateScore($values, ['biologi' => 0.40, 'kimia' => 0.20, 'mtk' => 0.15, 'fisika' => 0.05]),
'detail' => 'Bidang peternakan dan manajemen hewan ternak.'
],
[
'jurusan' => 'Manajemen Agribisnis',
'skor' => $this->calculateScore($values, ['ekonomi' => 0.35, 'geografi' => 0.25, 'mtk' => 0.20, 'sosiologi' => 0.15]),
'detail' => 'Kombinasi bisnis dan pertanian yang sempurna.'
],
[
'jurusan' => 'Akuntansi',
'skor' => $this->calculateScore($values, ['ekonomi' => 0.40, 'mtk' => 0.25, 'sejarah' => 0.15, 'sosiologi' => 0.10]),
'detail' => 'Untuk yang tertarik dengan keuangan dan akuntansi.'
],
[
'jurusan' => 'Bahasa Komunikasi',
'skor' => $this->calculateScore($values, ['sosiologi' => 0.30, 'sejarah' => 0.25, 'ekonomi' => 0.20, 'geografi' => 0.15]),
'detail' => 'Bidang komunikasi dan seni untuk yang ekspresif.'
],
];
// Sort by score descending
usort($majors, fn($a, $b) => $b['skor'] <=> $a['skor']);
return array_slice($majors, 0, 3);
}
/**
* Calculate score based on weighted values
*/
private function calculateScore($values, $weights)
{
$score = 0;
$totalWeight = 0;
foreach ($weights as $subject => $weight) {
if (isset($values[$subject])) {
$score += ($values[$subject] / 100) * $weight;
$totalWeight += $weight;
}
}
return $totalWeight > 0 ? round(($score / $totalWeight) * 100, 1) : 0;
}
/**
* Create sample chat history for a student
*/
private function createSampleChatHistory($userId, $recommendationId, $sessionId, $studentData)
{
$chatSamples = [
[
'pertanyaan' => 'Jurusan apa yang cocok untuk saya?',
'jawaban' => 'Berdasarkan nilai dan minat Anda, ' . $studentData['expected_major'] . ' adalah pilihan terbaik. Dengan prestasi di bidang ' . strtolower($studentData['prestasi']) . ', Anda memiliki potensi besar untuk sukses.'
],
[
'pertanyaan' => 'Mengapa jurusan tersebut cocok untuk saya?',
'jawaban' => 'Karena nilai ' . implode(', ', array_keys($studentData['values'])) . ' Anda menunjukkan keunggulan di area yang relevan dengan jurusan tersebut.'
],
];
foreach ($chatSamples as $index => $chat) {
\App\Models\ChatHistory::create([
'user_id' => $userId,
'id_sesi' => $sessionId,
'id_rekomendasi' => $recommendationId,
'pertanyaan' => $chat['pertanyaan'],
'jawaban' => $chat['jawaban'],
]);
}
}
}

View File

@ -0,0 +1,248 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Recommendation;
use App\Models\PolijeMajor;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class StudentWithAccurateRecommendationSeeder extends Seeder
{
public function run(): void
{
// Data siswa dengan nilai yang logis untuk setiap rekomendasi
$studentsData = [
// Siswa yang cocok untuk Teknologi Informasi
[
'name' => 'Rino Pratama',
'email' => 'rino.pratama@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 92, 'fisika' => 88, 'kimia' => 75, 'biologi' => 70],
'minat' => 'Logika Komputer',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Software Engineer',
'prestasi' => 'Juara 1 Kompetisi Coding',
],
[
'name' => 'Budi Santoso',
'email' => 'budi.santoso@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 88, 'fisika' => 85, 'kimia' => 70, 'biologi' => 68],
'minat' => 'Pemrograman',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Web Developer',
'prestasi' => 'Peserta Hackathon 2025',
],
// Siswa yang cocok untuk Teknik
[
'name' => 'Adi Wijaya',
'email' => 'adi.wijaya@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 85, 'fisika' => 90, 'kimia' => 70, 'biologi' => 65],
'minat' => 'Mekanika & Energi',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Insinyur Mesin',
'prestasi' => 'Juara 2 Robotika',
],
[
'name' => 'Dani Pratama',
'email' => 'dani.pratama@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 86, 'fisika' => 88, 'kimia' => 72, 'biologi' => 66],
'minat' => 'Elektronika',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Teknisi Elektronik',
'prestasi' => 'Aktif di klub robotika',
],
// Siswa yang cocok untuk Kesehatan
[
'name' => 'Siti Nurhaliza',
'email' => 'siti.nurhaliza@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 80, 'fisika' => 70, 'kimia' => 88, 'biologi' => 92],
'minat' => 'Ilmu Hayat & Pelayanan',
'pref_studi' => 'Kesehatan & Ilmu Hayat',
'cita_cita' => 'Perawat Profesional',
'prestasi' => 'Juara Olimpiade Biologi',
],
[
'name' => 'Tina Susanti',
'email' => 'tina.susanti@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 78, 'fisika' => 68, 'kimia' => 90, 'biologi' => 89],
'minat' => 'Farmasi & Kesehatan',
'pref_studi' => 'Kesehatan & Ilmu Hayat',
'cita_cita' => 'Apoteker',
'prestasi' => 'Aktif di PMR',
],
// Siswa yang cocok untuk Pertanian
[
'name' => 'Wayan Suparta',
'email' => 'wayan.suparta@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 75, 'fisika' => 72, 'kimia' => 82, 'biologi' => 88],
'minat' => 'Alam & Pertanian',
'pref_studi' => 'Pertanian & Lingkungan',
'cita_cita' => 'Ahli Pertanian',
'prestasi' => 'Peraih Medali Sains',
],
[
'name' => 'Bambang Hermawan',
'email' => 'bambang.hermawan@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 78, 'fisika' => 75, 'kimia' => 80, 'biologi' => 85],
'minat' => 'Ternak & Hewan',
'pref_studi' => 'Pertanian & Lingkungan',
'cita_cita' => 'Peternak Modern',
'prestasi' => 'Juara Pameran Ternak',
],
// Siswa cocok untuk Teknologi Pertanian
[
'name' => 'Hendra Sutrisno',
'email' => 'hendra.sutrisno@student.edu',
'kelompok_asal' => 'IPA',
'values' => ['mtk' => 87, 'fisika' => 86, 'kimia' => 76, 'biologi' => 78],
'minat' => 'Inovasi & Teknologi Pertanian',
'pref_studi' => 'Sains & Teknologi',
'cita_cita' => 'Engineer Pertanian',
'prestasi' => 'Penemu Alat Pertanian',
],
// Siswa IPS yang cocok untuk Bisnis/Ekonomi
[
'name' => 'Rina Handayani',
'email' => 'rina.handayani@student.edu',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 92, 'geografi' => 85, 'sosiologi' => 80, 'sejarah' => 78],
'minat' => 'Bisnis & Kewirausahaan',
'pref_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Entrepreneur',
'prestasi' => 'Juara Kompetisi Bisnis',
],
[
'name' => 'Agus Suryanto',
'email' => 'agus.suryanto@student.edu',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 88, 'geografi' => 82, 'sosiologi' => 85, 'sejarah' => 80],
'minat' => 'Manajemen & Akuntansi',
'pref_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Akuntan Profesional',
'prestasi' => 'Peserta Lomba Case Study',
],
// Siswa IPS untuk Agribisnis
[
'name' => 'Mardi Santoso',
'email' => 'mardi.santoso@student.edu',
'kelompok_asal' => 'IPS',
'values' => ['ekonomi' => 85, 'geografi' => 88, 'sosiologi' => 78, 'sejarah' => 75],
'minat' => 'Pertanian & Bisnis',
'pref_studi' => 'Bisnis & Manajemen',
'cita_cita' => 'Manager Agribisnis',
'prestasi' => 'Aktif di OSIS',
],
// Siswa IPS untuk Bahasa & Komunikasi
[
'name' => 'Nadia Putri',
'email' => 'nadia.putri@student.edu',
'kelompok_asal' => 'IPS',
'values' => ['sejarah' => 88, 'sosiologi' => 86, 'geografi' => 80, 'ekonomi' => 78],
'minat' => 'Bahasa & Komunikasi',
'pref_studi' => 'Sosial & Humaniora',
'cita_cita' => 'Jurnalis Profesional',
'prestasi' => 'Penulis Artikel Terpercaya',
],
[
'name' => 'Lisa Maharani',
'email' => 'lisa.maharani@student.edu',
'kelompok_asal' => 'IPS',
'values' => ['sejarah' => 85, 'sosiologi' => 82, 'geografi' => 85, 'ekonomi' => 76],
'minat' => 'Pariwisata & Budaya',
'pref_studi' => 'Sosial & Humaniora',
'cita_cita' => 'Tour Guide Profesional',
'prestasi' => 'Peserta Pelatihan Tour Guide',
],
];
$majorNames = PolijeMajor::pluck('nama_jurusan')->toArray();
foreach ($studentsData as $data) {
// Buat user siswa
$user = User::firstOrCreate(
['email' => $data['email']],
[
'name' => $data['name'],
'password' => bcrypt('password'),
'role' => 'siswa',
'kelompok_asal' => $data['kelompok_asal'],
]
);
// Hitung skor rekomendasi berdasarkan bobot mapel
$scores = $this->calculateScores($data, $majorNames);
// Ambil top 3 dengan skor tertinggi
arsort($scores);
$topRecommendations = array_slice($scores, 0, 3, true);
// Buat data rekomendasi dengan ranking
$recommendations = [];
foreach ($topRecommendations as $majorName => $score) {
$recommendations[] = [
'jurusan' => $majorName,
'skor' => round($score, 2),
'detail' => "Rekomendasi berdasarkan nilai, minat, dan preferensi studi.",
];
}
// Simpan rekomendasi ke database
Recommendation::create([
'user_id' => $user->id,
'mtk' => $data['values']['mtk'] ?? 0,
'fisika' => $data['values']['fisika'] ?? 0,
'kimia' => $data['values']['kimia'] ?? 0,
'biologi' => $data['values']['biologi'] ?? 0,
'ekonomi' => $data['values']['ekonomi'] ?? 0,
'sejarah' => $data['values']['sejarah'] ?? 0,
'geografi' => $data['values']['geografi'] ?? 0,
'sosiologi' => $data['values']['sosiologi'] ?? 0,
'minat' => $data['minat'],
'preferensi_studi' => $data['pref_studi'],
'cita_cita' => $data['cita_cita'],
'prestasi' => $data['prestasi'],
'hasil_rekomendasi' => json_encode($recommendations),
]);
}
$this->command->info('✅ ' . count($studentsData) . ' siswa dengan rekomendasi akurat sudah ditambahkan!');
}
private function calculateScores(array $studentData, array $majorNames): array
{
$scores = [];
$majors = PolijeMajor::all()->keyBy('nama_jurusan');
foreach ($majorNames as $majorName) {
if (!isset($majors[$majorName])) continue;
$major = $majors[$majorName];
$bobot = $major->bobot_mapel;
$score = 0;
// Hitung skor berdasarkan bobot dan nilai siswa
foreach ($bobot as $subject => $weight) {
$value = $studentData['values'][$subject] ?? 0;
$score += ($value * $weight) / 100;
}
// Bonus untuk preferensi studi yang sesuai
if (in_array($studentData['pref_studi'], $major->preferensi_studi)) {
$score += 5;
}
$scores[$majorName] = $score;
}
return $scores;
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace Database\Seeders;
use App\Models\PolijeMajor;
use Illuminate\Database\Seeder;
class UpdateMajorsAccurateBobotSeeder extends Seeder
{
public function run(): void
{
// Data jurusan dengan bobot mapel yang akurat untuk Naive Bayes
$majorsData = [
[
'nama_jurusan' => 'Produksi Pertanian',
'bobot_mapel' => [
'mtk' => 0.15,
'fisika' => 0.15,
'kimia' => 0.30,
'biologi' => 0.40, // Tertinggi - biologi alam/tanaman
'ekonomi' => 0.25,
'geografi' => 0.35, // Tinggi - lokasi/iklim/lahan
'sejarah' => 0.10,
'sosiologi' => 0.15,
],
'keywords' => ['pertanian', 'petani', 'kebun', 'sawah', 'panen', 'tanaman', 'budidaya', 'agronomi', 'tanam', 'bercocok tanam', 'alam', 'hortikultura', 'pupuk', 'bibit', 'agroteknologi', 'perkebunan', 'pangan', 'ketahanan pangan', 'hidroponik', 'organik', 'lahan', 'irigasi', 'cuaca', 'musim'],
'preferensi_studi' => ['Pertanian & Lingkungan', 'Sains & Teknologi', 'Inovasi & Teknologi', 'Sustainable Development', 'Agribisnis Modern'],
],
[
'nama_jurusan' => 'Teknologi Pertanian',
'bobot_mapel' => [
'mtk' => 0.35, // Tinggi - rumus & hitungan
'fisika' => 0.35, // Tertinggi - mesin/energi/gerakan
'kimia' => 0.20,
'biologi' => 0.15,
'ekonomi' => 0.25,
'geografi' => 0.25,
'sejarah' => 0.15,
'sosiologi' => 0.15,
],
'keywords' => ['teknologi pertanian', 'mesin pertanian', 'inovasi', 'otomasi', 'pengolahan pangan', 'pangan', 'mekanisasi', 'teknologi pangan', 'alat pertanian', 'rekayasa', 'iot pertanian', 'smart farming', 'digital farming', 'kontrol kualitas', 'proses produksi', 'efisiensi', 'presisi pertanian'],
'preferensi_studi' => ['Sains & Teknologi', 'Inovasi & Teknologi', 'Pertanian & Lingkungan', 'Sustainable Development', 'Digital Transformation'],
],
[
'nama_jurusan' => 'Peternakan',
'bobot_mapel' => [
'mtk' => 0.20,
'fisika' => 0.15,
'kimia' => 0.25,
'biologi' => 0.45, // Tertinggi - ilmu hewan
'ekonomi' => 0.30, // Tinggi - pasar/bisnis ternak
'geografi' => 0.25,
'sejarah' => 0.10,
'sosiologi' => 0.15,
],
'keywords' => ['ternak', 'hewan', 'peternakan', 'peternak', 'sapi', 'ayam', 'unggas', 'kambing', 'susu', 'pakan', 'nutrisi hewan', 'veteriner', 'ikan', 'aquaculture', 'budidaya hewan', 'farm management', 'kesehatan hewan', 'produksi ternak', 'perikanan', 'kolam', 'perkembangbiakan', 'reproduksi'],
'preferensi_studi' => ['Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat', 'Agribisnis Modern', 'Sustainable Development', 'Sains & Teknologi'],
],
[
'nama_jurusan' => 'Manajemen Agribisnis',
'bobot_mapel' => [
'mtk' => 0.35, // Tinggi - akuntansi/perhitungan
'fisika' => 0.15,
'kimia' => 0.10,
'biologi' => 0.15,
'ekonomi' => 0.45, // Tertinggi - bisnis/pasar
'geografi' => 0.20,
'sejarah' => 0.15,
'sosiologi' => 0.20,
],
'keywords' => ['bisnis', 'agribisnis', 'usaha', 'entrepreneur', 'pengusaha', 'dagang', 'jual', 'pemasaran', 'kewirausahaan', 'manajemen', 'ekonomi pertanian', 'pasar', 'supply chain', 'logistik', 'analisis pasar', 'branding produk', 'akuntansi', 'keuangan', 'investasi', 'ekspor', 'strategi bisnis'],
'preferensi_studi' => ['Bisnis & Manajemen', 'Entrepreneurship & Inovasi', 'Pertanian & Lingkungan', 'Agribisnis Modern', 'Digital Transformation'],
],
[
'nama_jurusan' => 'Teknologi Informasi',
'bobot_mapel' => [
'mtk' => 0.50, // Tertinggi - logika/algoritma
'fisika' => 0.20,
'kimia' => 0.10,
'biologi' => 0.10,
'ekonomi' => 0.20,
'geografi' => 0.10,
'sejarah' => 0.15,
'sosiologi' => 0.15,
],
'keywords' => ['programmer', 'developer', 'coding', 'software', 'web', 'aplikasi', 'komputer', 'it', 'jaringan', 'hacker', 'game', 'data', 'ai', 'robot', 'ngoding', 'laptop', 'teknologi', 'digital', 'internet', 'programming', 'desain grafis', 'ui ux', 'mobile app', 'cloud', 'database', 'machine learning', 'cybersecurity', 'backend', 'frontend'],
'preferensi_studi' => ['Sains & Teknologi', 'Inovasi & Teknologi', 'Digital Transformation', 'Entrepreneurship & Inovasi', 'Problem Solving & Logic'],
],
[
'nama_jurusan' => 'Teknik',
'bobot_mapel' => [
'mtk' => 0.40, // Tinggi - perhitungan teknis
'fisika' => 0.45, // Tertinggi - gaya/energi/mekanika
'kimia' => 0.15,
'biologi' => 0.10,
'ekonomi' => 0.20,
'geografi' => 0.15,
'sejarah' => 0.15,
'sosiologi' => 0.10,
],
'keywords' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'instalasi', 'panel', 'mekanik', 'industri', 'manufaktur', 'pabrik', 'bangunan', 'konstruksi', 'sipil', 'energi', 'maintenance', 'mekatronika', 'instrumentasi', 'quality control', 'produksi', 'assembly'],
'preferensi_studi' => ['Sains & Teknologi', 'Industri & Manufaktur', 'Problem Solving & Logic', 'Inovasi & Teknologi', 'Infrastruktur & Pembangunan'],
],
[
'nama_jurusan' => 'Kesehatan',
'bobot_mapel' => [
'mtk' => 0.20,
'fisika' => 0.15,
'kimia' => 0.35, // Tinggi - farmasi/reaksi
'biologi' => 0.45, // Tertinggi - anatomi/fisiologi
'ekonomi' => 0.15,
'geografi' => 0.10,
'sejarah' => 0.10,
'sosiologi' => 0.30, // Tinggi - interaksi pasien
],
'keywords' => ['dokter', 'perawat', 'medis', 'gizi', 'kesehatan', 'pelayanan', 'terapis', 'obat', 'rumah sakit', 'klinik', 'farmasi', 'nutrisi', 'sanitasi', 'rawat', 'sehat', 'kesehatan masyarakat', 'laboratorium', 'diagnostik', 'wellness', 'vaksin', 'fisioterapi', 'psikologi', 'kebidanan'],
'preferensi_studi' => ['Kesehatan & Ilmu Hayat', 'Pelayanan & Sosial', 'Sains & Teknologi', 'Inovasi Medis', 'Humanitarian & Community'],
],
[
'nama_jurusan' => 'Bahasa, Komunikasi, dan Pariwisata',
'bobot_mapel' => [
'mtk' => 0.15, // Rendah - bukan fokus
'fisika' => 0.10,
'kimia' => 0.10,
'biologi' => 0.10,
'ekonomi' => 0.25, // Tinggi - pariwisata/bisnis
'geografi' => 0.35, // Tinggi - destinasi/lokasi wisata
'sejarah' => 0.35, // Tertinggi - konteks budaya/sejarah
'sosiologi' => 0.35, // Tertinggi - interaksi sosial/komunikasi
],
'keywords' => ['bahasa', 'komunikasi', 'pariwisata', 'tour guide', 'hotel', 'jurnalis', 'marketing', 'inggris', 'penerjemah', 'travel', 'wisata', 'hospitality', 'public speaking', 'media', 'broadcasting', 'content creator', 'humas', 'event', 'pelayanan tamu', 'budaya', 'sejarah', 'linguistik', 'turis', 'destinasi'],
'preferensi_studi' => ['Sosial & Humaniora', 'Bisnis & Manajemen', 'Kreativitas & Komunikasi', 'Entrepreneurship & Inovasi', 'Digital Marketing & Content'],
],
[
'nama_jurusan' => 'Bisnis',
'bobot_mapel' => [
'mtk' => 0.45, // Tinggi - akuntansi/analisis
'fisika' => 0.10,
'kimia' => 0.10,
'biologi' => 0.10,
'ekonomi' => 0.50, // Tertinggi - bisnis/ekonomi
'geografi' => 0.15,
'sejarah' => 0.15,
'sosiologi' => 0.20,
],
'keywords' => ['manager', 'pimpinan', 'bisnis', 'accounting', 'marketing', 'sales', 'kantor', 'keuangan', 'bank', 'akuntansi', 'hitung', 'administrasi', 'perbankan', 'ekonomi', 'uang', 'investasi', 'pajak', 'wirausaha', 'audit', 'finance', 'analisis bisnis', 'customer', 'strategi', 'leadership'],
'preferensi_studi' => ['Bisnis & Manajemen', 'Entrepreneurship & Inovasi', 'Kepemimpinan & Manajemen', 'Digital Transformation', 'Analisis & Problem Solving'],
],
];
foreach ($majorsData as $data) {
PolijeMajor::where('nama_jurusan', $data['nama_jurusan'])
->update([
'bobot_mapel' => $data['bobot_mapel'],
'keywords' => $data['keywords'],
'preferensi_studi' => $data['preferensi_studi'],
]);
}
$this->command->info('✅ Bobot mapel semua jurusan sudah diperbarui dengan akurat!');
}
}

View File

@ -49,6 +49,16 @@ def _is_authorized() -> bool:
return token == BACKEND_TOKEN
def _http_error(message: str, status_code: int, detail: str = "") -> Any:
payload = {
"success": False,
"message": message,
}
if detail:
payload["error"] = detail
return jsonify(payload), status_code
def _extract_text(data: Dict[str, Any]) -> str:
return (
data.get("candidates", [{}])[0]
@ -65,13 +75,13 @@ def _load_majors_file() -> Dict[str, Any]:
except FileNotFoundError:
return {
"ok": False,
"message": f"File data jurusan tidak ditemukan: {MAJORS_FILE_PATH}",
"message": "Data jurusan belum tersedia.",
"data": {"majors": []},
}
except json.JSONDecodeError as exc:
return {
"ok": False,
"message": f"Format JSON tidak valid: {exc}",
"message": "Data jurusan tidak dapat dibaca.",
"data": {"majors": []},
}
@ -166,10 +176,16 @@ def chat() -> Any:
_log("[PY-BACKEND] POST /api/chat")
if not _is_authorized():
_log("[PY-BACKEND] Unauthorized request")
return jsonify({"success": False, "message": "Unauthorized"}), 401
return _http_error(
"Akses ke layanan chatbot ditolak.",
401,
)
if not GEMINI_API_KEY:
return jsonify({"success": False, "message": "GEMINI_API_KEY belum diset di backend Python"}), 500
return _http_error(
"Layanan chatbot belum siap digunakan.",
500,
)
body = request.get_json(silent=True) or {}
payload = body.get("payload")
@ -177,7 +193,10 @@ def chat() -> Any:
if not isinstance(payload, dict) or not payload.get("contents"):
_log("[PY-BACKEND] Invalid payload")
return jsonify({"success": False, "message": "payload tidak valid"}), 422
return _http_error(
"Pesan belum dapat diproses. Silakan perjelas pertanyaan Anda.",
422,
)
majors_result = _load_majors_file()
if majors_result["ok"]:
@ -207,19 +226,30 @@ def chat() -> Any:
if text:
_log(f"[PY-BACKEND] Success using model: {model}")
return jsonify({"success": True, "message": text, "model": model})
last_error = "Respons model tidak berisi teks"
last_error = "Jawaban dari layanan tidak ditemukan."
continue
if resp.status_code in (404, 429):
if resp.status_code == 429:
time.sleep(1)
last_error = f"Model {model} gagal dengan status {resp.status_code}"
if resp.status_code == 429:
last_error = (
"Layanan chatbot sedang ramai digunakan. Silakan tunggu sebentar lalu coba lagi."
)
else:
last_error = "Layanan chatbot sementara tidak tersedia."
continue
last_error = f"Gemini error {resp.status_code}: {resp.text[:300]}"
last_error = (
"Terjadi gangguan pada layanan chatbot."
)
_log(f"[PY-BACKEND] Failed all models: {last_error}")
return jsonify({"success": False, "message": "Semua model gagal", "error": last_error}), 502
return _http_error(
"Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.",
502,
last_error,
)
if __name__ == "__main__":

View File

@ -0,0 +1,30 @@
2026-04-23 00:18:45,831 INFO WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.18.25:5000
2026-04-23 00:18:45,878 INFO Press CTRL+C to quit
2026-04-23 00:18:45,921 INFO * Restarting with stat
2026-04-23 00:18:48,881 WARNING * Debugger is active!
2026-04-23 00:18:48,891 INFO * Debugger PIN: 786-713-650
2026-04-23 00:35:47,917 INFO WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.18.25:5000
2026-04-23 00:35:48,031 INFO Press CTRL+C to quit
2026-04-23 00:35:48,180 INFO * Restarting with stat
2026-04-23 00:35:50,518 WARNING * Debugger is active!
2026-04-23 00:35:50,529 INFO * Debugger PIN: 786-713-650
2026-04-27 07:40:08,029 INFO WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.18.25:5000
2026-04-27 07:40:08,057 INFO Press CTRL+C to quit
2026-04-27 07:40:08,145 INFO * Restarting with stat
2026-04-27 07:41:46,436 INFO WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.18.25:5000
2026-04-27 07:41:46,446 INFO Press CTRL+C to quit
2026-04-27 07:41:46,458 INFO * Restarting with stat
2026-04-27 07:41:47,441 WARNING * Debugger is active!
2026-04-27 07:41:47,445 INFO * Debugger PIN: 531-826-879

View File

@ -23,40 +23,41 @@
</div>
</div>
<!-- Kelompok Distribution -->
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
<h3 class="text-lg font-bold text-maroon mb-4">📊 Siswa per Kelompok</h3>
<div class="space-y-3">
@foreach($kelompokStats as $stat)
<div class="flex items-center gap-3">
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
<div class="flex-1 h-6 bg-gray-200 rounded">
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
{{ $stat->count }}
</div>
</div>
</div>
@endforeach
<!-- Rekomendasi Distribution Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
<h3 class="text-lg font-bold text-maroon mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
<div style="position: relative; height: 300px;">
<canvas id="chartRecommendations"></canvas>
</div>
</div>
<!-- Top Recommended Majors -->
<!-- Kelompok Distribution Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-indigo-400">
<h3 class="text-lg font-bold text-maroon mb-4">📈 Distribusi Siswa per Kelompok</h3>
<div style="position: relative; height: 300px;">
<canvas id="chartKelompok"></canvas>
</div>
</div>
</div>
<!-- Kelompok Distribution -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Kelompok Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
<h3 class="text-lg font-bold text-maroon mb-4">📊 Siswa per Kelompok</h3>
<div style="position: relative; height: 250px;">
<canvas id="chartKelompokPie"></canvas>
</div>
</div>
<!-- Top Recommended Majors Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Top Recommended Majors</h3>
@if($topMajors->isNotEmpty())
<div class="space-y-3">
@foreach($topMajors as $major)
<div class="flex items-center gap-3">
<span class="text-sm font-semibold text-gray-700 flex-1 truncate">{{ $major->major_name }}</span>
<span class="px-3 py-1 rounded bg-green-100 text-green-800 font-bold text-sm">{{ $major->count }}</span>
</div>
@endforeach
</div>
@else
<p class="text-gray-500 text-sm">Belum ada data rekomendasi</p>
@endif
<div style="position: relative; height: 250px;">
<canvas id="chartTopMajors"></canvas>
</div>
</div>
</div>
@ -135,3 +136,140 @@
@endif
</div>
@endsection
@section('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
<script>
// Chart 1: Rekomendasi Distribution
const chartRecommendationsCtx = document.getElementById('chartRecommendations').getContext('2d');
const chartRecommendations = new Chart(chartRecommendationsCtx, {
type: 'doughnut',
data: {
labels: @json($chartMajorNames),
datasets: [{
data: @json($chartMajorCounts),
backgroundColor: [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0'
],
borderColor: '#fff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
font: { size: 11 },
padding: 10
}
}
}
}
});
// Chart 2: Kelompok Distribution
const chartKelompokCtx = document.getElementById('chartKelompok').getContext('2d');
const chartKelompok = new Chart(chartKelompokCtx, {
type: 'bar',
data: {
labels: @json($chartKelompokNames),
datasets: [{
label: 'Jumlah Siswa',
data: @json($chartKelompokCounts),
backgroundColor: ['#0369A1', '#D97706'],
borderColor: ['#0369A1', '#D97706'],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
font: { size: 11 }
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
font: { size: 10 }
}
}
}
}
});
// Chart 3: Kelompok Pie Chart
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
type: 'pie',
data: {
labels: @json($chartKelompokNames),
datasets: [{
data: @json($chartKelompokCounts),
backgroundColor: ['#0369A1', '#D97706'],
borderColor: '#fff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
font: { size: 11 },
padding: 10
}
}
}
}
});
// Chart 4: Top Majors Horizontal Bar Chart
const chartTopMajorsCtx = document.getElementById('chartTopMajors').getContext('2d');
const chartTopMajors = new Chart(chartTopMajorsCtx, {
type: 'bar',
data: {
labels: @json($topMajorsChart),
datasets: [{
label: 'Jumlah Rekomendasi',
data: @json($topMajorsCounts),
backgroundColor: '#36A2EB',
borderColor: '#36A2EB',
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
font: { size: 11 }
}
}
},
scales: {
x: {
beginAtZero: true,
ticks: {
font: { size: 10 }
}
}
}
}
});
</script>
@endsection

View File

@ -140,7 +140,7 @@
<div class="sidebar-brand-icon">🎓</div>
<div>
<p class="text-white font-bold text-sm leading-tight">SPK Jurusan</p>
<p class="text-xs text-slate-400">Admin Panel</p>
<p class="text-xs text-white">Admin Panel</p>
</div>
</div>
</div>
@ -178,8 +178,8 @@
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
</div>
<div class="flex-1 min-w-0">
<p class="text-xs font-medium text-slate-300 truncate">{{ Auth::user()->name }}</p>
<p class="text-xs text-slate-500">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
<p class="text-xs font-medium text-white truncate">{{ Auth::user()->name }}</p>
<p class="text-xs text-white">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
</div>
</div>
</div>

View File

@ -13,120 +13,132 @@
</a>
</div>
<!-- Profile Card -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-maroon">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p class="text-gray-600 text-sm font-semibold">Nama</p>
<p class="text-xl font-bold text-maroon mt-1">{{ $student->name }}</p>
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Email</p>
<p class="text-gray-800 mt-1">{{ $student->email }}</p>
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">NIS</p>
<p class="text-gray-800 font-semibold mt-1">{{ $student->nis ?? '-' }}</p>
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Kelompok Asal</p>
@if($student->kelompok_asal)
<p class="mt-1">
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
{{ $student->kelompok_asal }}
</span>
</p>
@else
<p class="text-gray-500 mt-1">-</p>
@endif
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Foto Profil</p>
<!-- Profile Header Card - Horizontal Layout -->
<div class="bg-gradient-to-r from-maroon to-teal-600 rounded-lg shadow-lg p-8 mb-6 text-white">
<div class="flex gap-8 items-start">
<!-- Avatar Section -->
<div class="flex-shrink-0">
@if($student->foto)
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 rounded-lg object-cover mt-2 border-2 border-maroon">
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-32 h-32 rounded-xl object-cover border-4 border-white shadow-lg">
@else
<p class="text-gray-500 mt-1">-</p>
<div class="w-32 h-32 rounded-xl bg-white bg-opacity-20 flex items-center justify-center text-3xl font-bold">
{{ strtoupper(substr($student->name, 0, 1)) }}
</div>
@endif
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p>
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p>
</div>
</div>
</div>
<!-- Rekomendasi -->
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
@if($recommendations->isNotEmpty())
<div class="space-y-4">
@foreach($recommendations as $rec)
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition">
<div class="flex justify-between items-start mb-3">
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
<span class="px-2 py-1 rounded text-xs font-bold bg-blue-100 text-blue-800">Rekomendasi #{{ $loop->index + 1 }}</span>
</div>
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
<div class="mt-3">
<p class="text-xs font-semibold text-gray-600 mb-2">Top 3 Rekomendasi:</p>
<div class="space-y-2">
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
<div class="flex items-center justify-between">
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
@php $skorVal = $hasil['skor'] ?? 0; @endphp
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#DBEAFE' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#1e40af' : '#6B7280' }};">
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
</span>
</div>
@endforeach
</div>
</div>
<!-- Info Section -->
<div class="flex-1">
<h3 class="text-3xl font-bold mb-4">{{ $student->name }}</h3>
<div class="grid grid-cols-2 gap-4 md:gap-6">
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">NIS</p>
<p class="text-lg font-bold">{{ $student->nis ?? '-' }}</p>
</div>
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">Kelompok</p>
@if($student->kelompok_asal)
<span class="inline-block px-3 py-1 rounded-full text-sm font-bold bg-white text-maroon mt-1">
{{ $student->kelompok_asal }}
</span>
@else
<p class="text-lg font-bold">-</p>
@endif
<div class="mt-3 p-3 bg-gray-50 rounded text-xs text-gray-600">
<p><strong>Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
</div>
</div>
@endforeach
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">Email</p>
<p class="text-sm font-semibold break-all">{{ $student->email }}</p>
</div>
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">Terdaftar</p>
<p class="text-sm font-semibold">{{ $student->created_at->format('d M Y') }}</p>
</div>
</div>
</div>
@else
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
@endif
</div>
</div>
<!-- Chat History -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-maroon">💬 Chat History ({{ count($chatHistories) }})</h3>
@if(count($chatHistories) > 0)
<a href="{{ route('admin.student.chat', $student->id) }}" class="bg-blue-500 text-white font-semibold py-2 px-3 rounded-lg hover:bg-blue-600 transition text-xs">
Lihat Semua
</a>
@endif
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left Column: Rekomendasi (2/3 width) -->
<div class="lg:col-span-2">
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
@if($recommendations->isNotEmpty())
<div class="space-y-4">
@foreach($recommendations as $rec)
<div class="border-2 border-green-100 rounded-lg p-4 hover:shadow-md hover:border-green-400 transition">
<div class="flex justify-between items-center mb-3">
<p class="text-xs font-semibold text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
<span class="px-2 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700">Rekomendasi #{{ $loop->index + 1 }}</span>
</div>
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
<div class="space-y-2">
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
<div class="flex items-center justify-between bg-gradient-to-r {{ $idx === 0 ? 'from-green-50 to-transparent' : 'from-gray-50 to-transparent' }} p-3 rounded">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full {{ $idx === 0 ? 'bg-green-500 text-white' : 'bg-gray-300 text-white' }} text-xs font-bold">
{{ $idx + 1 }}
</span>
<span class="text-sm font-semibold text-gray-800">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
</div>
@php $skorVal = $hasil['skor'] ?? 0; @endphp
<span class="px-3 py-1 rounded-full text-xs font-bold {{ $idx === 0 ? 'bg-green-200 text-green-800' : 'bg-gray-200 text-gray-800' }}">
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
</span>
</div>
@endforeach
</div>
@if(isset($rec->minat) || isset($rec->cita_cita))
<div class="mt-3 p-3 bg-blue-50 border-l-4 border-blue-300 rounded">
<p class="text-xs text-gray-700"><strong>📝 Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>🎓 Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
</div>
@endif
@endif
</div>
@endforeach
</div>
@else
<div class="text-center py-8 text-gray-500">
<p class="text-sm">Siswa belum melakukan rekomendasi</p>
</div>
@endif
</div>
</div>
@if($chatHistories->isNotEmpty())
<div class="space-y-3 max-h-96 overflow-y-auto">
@foreach($chatHistories as $chat)
<div class="border-b pb-3 last:border-b-0">
<div class="flex justify-between items-start mb-2">
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p>
</div>
<div class="bg-blue-50 border-l-4 border-blue-400 p-3 rounded mb-2">
<p class="text-xs font-semibold text-gray-700 mb-1">👤 Pertanyaan Siswa:</p>
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p>
</div>
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded">
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p>
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p>
</div>
<!-- Right Column: Chat History (1/3 width) -->
<div class="lg:col-span-1">
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-maroon">💬 Chat</h3>
<span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
</div>
@if($chatHistories->isNotEmpty())
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
@foreach($chatHistories->take(5) as $chat)
<div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
<p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
<p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
<p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
</div>
@endforeach
</div>
@endforeach
@if(count($chatHistories) > 5)
<a href="{{ route('admin.student.chat', $student->id) }}" class="block mt-3 text-center text-xs text-blue-600 font-semibold hover:text-blue-800">
Lihat Semua ({{ count($chatHistories) }})
</a>
@endif
@else
<p class="text-gray-500 text-xs text-center py-4">Belum ada chat</p>
@endif
</div>
@else
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
@endif
</div>
</div>
@endsection

View File

@ -28,39 +28,41 @@
</div>
</div>
<!-- Kelompok Distribution & Top Majors -->
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-teal-500">
<h3 class="text-lg font-bold text-bk mb-4">📊 Siswa per Kelompok</h3>
<div class="space-y-3">
@foreach($kelompokStats as $stat)
<div class="flex items-center gap-3">
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
<div class="flex-1 h-6 bg-gray-200 rounded">
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
{{ $stat->count }}
</div>
</div>
</div>
@endforeach
<!-- Rekomendasi Distribution Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
<h3 class="text-lg font-bold text-bk mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
<div style="position: relative; height: 300px;">
<canvas id="chartRecommendations"></canvas>
</div>
</div>
<!-- Kelompok Distribution Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-indigo-400">
<h3 class="text-lg font-bold text-bk mb-4">📈 Distribusi Siswa per Kelompok</h3>
<div style="position: relative; height: 300px;">
<canvas id="chartKelompok"></canvas>
</div>
</div>
</div>
<!-- Kelompok Distribution & Top Majors -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Kelompok Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-teal-500">
<h3 class="text-lg font-bold text-bk mb-4">📊 Siswa per Kelompok</h3>
<div style="position: relative; height: 250px;">
<canvas id="chartKelompokPie"></canvas>
</div>
</div>
<!-- Top Recommended Majors Chart -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
<h3 class="text-lg font-bold text-bk mb-4">🎯 Jurusan Terpopuler</h3>
@if($topMajors->isNotEmpty())
<div class="space-y-3">
@foreach($topMajors as $major)
<div class="flex items-center gap-3">
<span class="text-sm font-semibold text-gray-700 flex-1 truncate">{{ $major->major_name }}</span>
<span class="px-3 py-1 rounded bg-green-100 text-green-800 font-bold text-sm">{{ $major->count }}</span>
</div>
@endforeach
</div>
@else
<p class="text-gray-500 text-sm">Belum ada data rekomendasi</p>
@endif
<div style="position: relative; height: 250px;">
<canvas id="chartTopMajors"></canvas>
</div>
</div>
</div>
@ -139,3 +141,140 @@
@endif
</div>
@endsection
@section('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
<script>
// Chart 1: Rekomendasi Distribution
const chartRecommendationsCtx = document.getElementById('chartRecommendations').getContext('2d');
const chartRecommendations = new Chart(chartRecommendationsCtx, {
type: 'doughnut',
data: {
labels: @json($chartMajorNames),
datasets: [{
data: @json($chartMajorCounts),
backgroundColor: [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0'
],
borderColor: '#fff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
font: { size: 11 },
padding: 10
}
}
}
}
});
// Chart 2: Kelompok Distribution Bar
const chartKelompokCtx = document.getElementById('chartKelompok').getContext('2d');
const chartKelompok = new Chart(chartKelompokCtx, {
type: 'bar',
data: {
labels: @json($chartKelompokNames),
datasets: [{
label: 'Jumlah Siswa',
data: @json($chartKelompokCounts),
backgroundColor: ['#0369A1', '#D97706'],
borderColor: ['#0369A1', '#D97706'],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
font: { size: 11 }
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
font: { size: 10 }
}
}
}
}
});
// Chart 3: Kelompok Pie Chart
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
type: 'pie',
data: {
labels: @json($chartKelompokNames),
datasets: [{
data: @json($chartKelompokCounts),
backgroundColor: ['#0369A1', '#D97706'],
borderColor: '#fff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
font: { size: 11 },
padding: 10
}
}
}
}
});
// Chart 4: Top Majors Horizontal Bar Chart
const chartTopMajorsCtx = document.getElementById('chartTopMajors').getContext('2d');
const chartTopMajors = new Chart(chartTopMajorsCtx, {
type: 'bar',
data: {
labels: @json($topMajorsChart),
datasets: [{
label: 'Jumlah Rekomendasi',
data: @json($topMajorsCounts),
backgroundColor: '#36A2EB',
borderColor: '#36A2EB',
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: {
font: { size: 11 }
}
}
},
scales: {
x: {
beginAtZero: true,
ticks: {
font: { size: 10 }
}
}
}
}
});
</script>
@endsection

View File

@ -13,120 +13,132 @@
</a>
</div>
<!-- Profile Card -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-teal-500">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p class="text-gray-600 text-sm font-semibold">Nama</p>
<p class="text-xl font-bold text-bk mt-1">{{ $student->name }}</p>
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Email</p>
<p class="text-gray-800 mt-1">{{ $student->email }}</p>
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">NIS</p>
<p class="text-gray-800 font-semibold mt-1">{{ $student->nis ?? '-' }}</p>
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Kelompok Asal</p>
@if($student->kelompok_asal)
<p class="mt-1">
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
{{ $student->kelompok_asal }}
</span>
</p>
@else
<p class="text-gray-500 mt-1">-</p>
@endif
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Foto Profil</p>
<!-- Profile Header Card - Horizontal Layout -->
<div class="bg-gradient-to-r from-teal-600 to-teal-400 rounded-lg shadow-lg p-8 mb-6 text-white">
<div class="flex gap-8 items-start">
<!-- Avatar Section -->
<div class="flex-shrink-0">
@if($student->foto)
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 rounded-lg object-cover mt-2 border-2 border-teal-500">
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-32 h-32 rounded-xl object-cover border-4 border-white shadow-lg">
@else
<p class="text-gray-500 mt-1">-</p>
<div class="w-32 h-32 rounded-xl bg-white bg-opacity-20 flex items-center justify-center text-3xl font-bold">
{{ strtoupper(substr($student->name, 0, 1)) }}
</div>
@endif
</div>
<div>
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p>
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p>
</div>
</div>
</div>
<!-- Rekomendasi -->
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
<h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
@if($recommendations->isNotEmpty())
<div class="space-y-4">
@foreach($recommendations as $rec)
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition">
<div class="flex justify-between items-start mb-3">
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
<span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-800">Rekomendasi #{{ $loop->index + 1 }}</span>
</div>
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
<div class="mt-3">
<p class="text-xs font-semibold text-gray-600 mb-2">Top 3 Rekomendasi:</p>
<div class="space-y-2">
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
<div class="flex items-center justify-between">
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
@php $skorVal = $hasil['skor'] ?? 0; @endphp
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#CCFBF1' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#0f766e' : '#6B7280' }};">
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
</span>
</div>
@endforeach
</div>
</div>
<!-- Info Section -->
<div class="flex-1">
<h3 class="text-3xl font-bold mb-4">{{ $student->name }}</h3>
<div class="grid grid-cols-2 gap-4 md:gap-6">
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">NIS</p>
<p class="text-lg font-bold">{{ $student->nis ?? '-' }}</p>
</div>
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">Kelompok</p>
@if($student->kelompok_asal)
<span class="inline-block px-3 py-1 rounded-full text-sm font-bold bg-white text-teal-600 mt-1">
{{ $student->kelompok_asal }}
</span>
@else
<p class="text-lg font-bold">-</p>
@endif
<div class="mt-3 p-3 bg-gray-50 rounded text-xs text-gray-600">
<p><strong>Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
</div>
</div>
@endforeach
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">Email</p>
<p class="text-sm font-semibold break-all">{{ $student->email }}</p>
</div>
<div>
<p class="text-white text-opacity-80 text-sm font-semibold">Terdaftar</p>
<p class="text-sm font-semibold">{{ $student->created_at->format('d M Y') }}</p>
</div>
</div>
</div>
@else
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
@endif
</div>
</div>
<!-- Chat History -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-bk">💬 Chat History ({{ count($chatHistories) }})</h3>
@if(count($chatHistories) > 0)
<a href="{{ route('bk.student.chat', $student->id) }}" class="bg-teal-500 text-white font-semibold py-2 px-3 rounded-lg hover:bg-teal-600 transition text-xs">
Lihat Semua
</a>
@endif
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left Column: Rekomendasi (2/3 width) -->
<div class="lg:col-span-2">
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
<h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
@if($recommendations->isNotEmpty())
<div class="space-y-4">
@foreach($recommendations as $rec)
<div class="border-2 border-green-100 rounded-lg p-4 hover:shadow-md hover:border-green-400 transition">
<div class="flex justify-between items-center mb-3">
<p class="text-xs font-semibold text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
<span class="px-2 py-1 rounded-full text-xs font-bold bg-teal-100 text-teal-700">Rekomendasi #{{ $loop->index + 1 }}</span>
</div>
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
<div class="space-y-2">
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
<div class="flex items-center justify-between bg-gradient-to-r {{ $idx === 0 ? 'from-teal-50 to-transparent' : 'from-gray-50 to-transparent' }} p-3 rounded">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full {{ $idx === 0 ? 'bg-teal-500 text-white' : 'bg-gray-300 text-white' }} text-xs font-bold">
{{ $idx + 1 }}
</span>
<span class="text-sm font-semibold text-gray-800">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
</div>
@php $skorVal = $hasil['skor'] ?? 0; @endphp
<span class="px-3 py-1 rounded-full text-xs font-bold {{ $idx === 0 ? 'bg-teal-200 text-teal-800' : 'bg-gray-200 text-gray-800' }}">
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
</span>
</div>
@endforeach
</div>
@if(isset($rec->minat) || isset($rec->cita_cita))
<div class="mt-3 p-3 bg-blue-50 border-l-4 border-blue-300 rounded">
<p class="text-xs text-gray-700"><strong>📝 Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>🎓 Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
</div>
@endif
@endif
</div>
@endforeach
</div>
@else
<div class="text-center py-8 text-gray-500">
<p class="text-sm">Siswa belum melakukan rekomendasi</p>
</div>
@endif
</div>
</div>
@if($chatHistories->isNotEmpty())
<div class="space-y-3 max-h-96 overflow-y-auto">
@foreach($chatHistories as $chat)
<div class="border-b pb-3 last:border-b-0">
<div class="flex justify-between items-start mb-2">
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p>
</div>
<div class="bg-blue-50 border-l-4 border-blue-400 p-3 rounded mb-2">
<p class="text-xs font-semibold text-gray-700 mb-1">👤 Pertanyaan Siswa:</p>
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p>
</div>
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded">
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p>
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p>
</div>
<!-- Right Column: Chat History (1/3 width) -->
<div class="lg:col-span-1">
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-bk">💬 Chat</h3>
<span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
</div>
@if($chatHistories->isNotEmpty())
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
@foreach($chatHistories->take(5) as $chat)
<div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
<p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
<p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
<p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
</div>
@endforeach
</div>
@endforeach
@if(count($chatHistories) > 5)
<a href="{{ route('bk.student.chat', $student->id) }}" class="block mt-3 text-center text-xs text-teal-600 font-semibold hover:text-teal-800">
Lihat Semua ({{ count($chatHistories) }})
</a>
@endif
@else
<p class="text-gray-500 text-xs text-center py-4">Belum ada chat</p>
@endif
</div>
@else
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
@endif
</div>
</div>
@endsection

View File

@ -25,85 +25,124 @@
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
<div>
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">History Rekomendasi</h1>
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Semua Analisis Anda</p>
</div>
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
Kembali Dashboard
</a>
<h1 class="text-2xl md:text-3xl font-bold">📊 History Rekomendasi</h1>
<p class="text-sm text-yellow-200 font-semibold mt-1">Semua Analisis Anda</p>
</div>
<a href="{{ url('/dashboard') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 rounded-lg hover:bg-yellow-300 transition text-sm">
Kembali Dashboard
</a>
</div>
</header>
<!-- Main Content -->
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
<div class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
@if($recommendations && $recommendations->count() > 0)
<div class="space-y-4 sm:space-y-6">
<div class="space-y-6">
@foreach($recommendations as $rec)
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-6 border-l-4 border-maroon hover:shadow-xl transition">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4 mb-4 sm:mb-6">
<div>
<h3 class="text-lg sm:text-xl font-bold text-maroon mb-2">
Analisis - {{ \Carbon\Carbon::parse($rec->created_at)->format('d M Y H:i') }}
</h3>
<p class="text-xs sm:text-sm text-gray-600">{{ $rec->created_at->diffForHumans() }}</p>
<div class="bg-white rounded-lg shadow-lg overflow-hidden border-l-4 border-maroon hover:shadow-xl transition">
<!-- Header Card -->
<div class="bg-gradient-to-r from-yellow-400 to-yellow-300 p-6 text-gray-800 cursor-pointer hover:bg-gradient-to-r hover:from-yellow-300 hover:to-yellow-200 transition" onclick="toggleDetail({{ $loop->index }})">
<div class="flex justify-between items-start gap-4">
<div class="flex-1">
<h3 class="text-2xl font-bold mb-2">📅 {{ \Carbon\Carbon::parse($rec->created_at)->format('d M Y H:i') }}</h3>
<p class="text-sm text-gray-700 font-semibold">{{ $rec->created_at->diffForHumans() }}</p>
</div>
<div class="flex-shrink-0">
<span class="inline-block px-4 py-2 rounded-full bg-white text-maroon font-bold text-sm">
Lihat Detail
</span>
</div>
</div>
<button onclick="toggleDetail({{ $loop->index }})" class="w-full sm:w-auto bg-maroon text-white font-bold py-2 px-4 rounded-lg hover:opacity-90 transition text-xs sm:text-sm">
Lihat Detail
</button>
</div>
<div id="detail-{{ $loop->index }}" class="hidden">
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mb-4 sm:mb-6">
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Minat</p>
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->minat ?? '-' }}</p>
</div>
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Pref. Belajar</p>
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->preferensi_studi ?? '-' }}</p>
</div>
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Cita-Cita</p>
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->cita_cita ?? '-', 15) }}</p>
</div>
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Prestasi</p>
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->prestasi ?? '-', 15) }}</p>
<!-- Detail Card (Hidden by default) -->
<div id="detail-{{ $loop->index }}" class="hidden border-t-4 border-yellow-200 p-6">
<!-- Input Data Summary -->
<div class="mb-8">
<h4 class="text-lg font-bold text-maroon mb-4">📝 Data Input</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500">
<p class="text-xs text-blue-700 font-semibold mb-1">💭 Minat</p>
<p class="text-sm font-bold text-gray-800">{{ $rec->minat ?? '-' }}</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg border-l-4 border-purple-500">
<p class="text-xs text-purple-700 font-semibold mb-1">🎓 Preferensi Studi</p>
<p class="text-sm font-bold text-gray-800">{{ $rec->preferensi_studi ?? '-' }}</p>
</div>
<div class="bg-green-50 p-4 rounded-lg border-l-4 border-green-500">
<p class="text-xs text-green-700 font-semibold mb-1">🎯 Cita-Cita</p>
<p class="text-sm font-bold text-gray-800">{{ $rec->cita_cita ?? '-' }}</p>
</div>
<div class="bg-red-50 p-4 rounded-lg border-l-4 border-red-500">
<p class="text-xs text-red-700 font-semibold mb-1">🏆 Prestasi</p>
<p class="text-sm font-bold text-gray-800">{{ $rec->prestasi ?? '-' }}</p>
</div>
</div>
</div>
<div class="bg-yellow-50 p-4 sm:p-6 rounded-lg">
<h4 class="font-bold text-maroon mb-3 sm:mb-4">Top 3 Rekomendasi Jurusan</h4>
<div class="space-y-2 sm:space-y-3">
@if($rec->hasil_rekomendasi)
<!-- Nilai Akademik -->
<div class="mb-8">
<h4 class="text-lg font-bold text-maroon mb-4">📚 Nilai Akademik</h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
@php
$subjects = [
['name' => 'Matematika', 'key' => 'mtk', 'icon' => '🔢'],
['name' => 'Fisika', 'key' => 'fisika', 'icon' => '⚡'],
['name' => 'Kimia', 'key' => 'kimia', 'icon' => '🧪'],
['name' => 'Biologi', 'key' => 'biologi', 'icon' => '🔬'],
['name' => 'Ekonomi', 'key' => 'ekonomi', 'icon' => '💰'],
['name' => 'Geografi', 'key' => 'geografi', 'icon' => '🌍'],
['name' => 'Sosiologi', 'key' => 'sosiologi', 'icon' => '👥'],
['name' => 'Sejarah', 'key' => 'sejarah', 'icon' => '📜'],
];
@endphp
@foreach($subjects as $subject)
@if($rec->{$subject['key']} !== null)
<div class="bg-gray-50 p-3 rounded-lg border border-gray-200 hover:border-maroon transition">
<p class="text-xs text-gray-600 font-semibold mb-1">{{ $subject['icon'] }} {{ $subject['name'] }}</p>
<p class="text-lg font-bold text-maroon">{{ $rec->{$subject['key']} }}</p>
</div>
@endif
@endforeach
</div>
</div>
<!-- Top 3 Rekomendasi Jurusan -->
<div>
<h4 class="text-lg font-bold text-maroon mb-4">🎯 Top 3 Rekomendasi Jurusan</h4>
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
<div class="space-y-3">
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $index => $hasil)
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-4 p-3 sm:p-4 bg-white rounded-lg border border-yellow-300">
<div class="flex items-center gap-3 sm:gap-4">
<span class="text-lg sm:text-xl font-bold text-yellow-500">{{ $index + 1 }}</span>
<span class="text-sm sm:text-base font-bold text-maroon">{{ $hasil['jurusan'] ?? '-' }}</span>
<div class="flex items-center justify-between p-4 bg-gradient-to-r {{ $index === 0 ? 'from-yellow-50 to-yellow-100' : ($index === 1 ? 'from-gray-50 to-gray-100' : 'from-orange-50 to-orange-100') }} rounded-lg border-l-4 {{ $index === 0 ? 'border-yellow-500' : ($index === 1 ? 'border-gray-500' : 'border-orange-500') }}">
<div class="flex items-center gap-4 flex-1">
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full {{ $index === 0 ? 'bg-yellow-500 text-white' : ($index === 1 ? 'bg-gray-500 text-white' : 'bg-orange-500 text-white') }} font-bold text-lg">
{{ $index + 1 }}
</span>
<div>
<p class="text-sm font-bold text-gray-600">Rekomendasi {{ $index + 1 }}</p>
<p class="text-lg font-bold text-gray-800">{{ $hasil['jurusan'] ?? '-' }}</p>
</div>
</div>
<span class="text-xs sm:text-sm bg-maroon text-white px-3 py-1 rounded-full font-bold">
@php $skorVal = $hasil['skor'] ?? 0; @endphp
@php $skorVal = $hasil['skor'] ?? 0; @endphp
<span class="px-4 py-2 rounded-full font-bold text-white {{ $index === 0 ? 'bg-yellow-500' : ($index === 1 ? 'bg-gray-500' : 'bg-orange-500') }}">
{{ number_format(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
</span>
</div>
@endforeach
@endif
</div>
</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="bg-white rounded-lg shadow-lg p-8 sm:p-12 text-center">
<div class="text-5xl sm:text-6xl mb-4">📊</div>
<h3 class="text-xl sm:text-2xl font-bold text-maroon mb-2">Belum Ada History</h3>
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-6">Anda belum melakukan analisis rekomendasi. Mulai sekarang!</p>
<a href="{{ url('/rekomendasi') }}" class="inline-block bg-maroon text-white font-bold py-2 sm:py-3 px-6 sm:px-8 rounded-lg hover:opacity-90 transition text-sm sm:text-base">
Mulai Analisis
<div class="bg-white rounded-lg shadow-lg p-12 text-center">
<div class="text-6xl mb-4">📊</div>
<h3 class="text-2xl font-bold text-maroon mb-2">Belum Ada History</h3>
<p class="text-gray-700 mb-6">Anda belum melakukan analisis rekomendasi. Mulai sekarang untuk melihat history!</p>
<a href="{{ url('/rekomendasi') }}" class="inline-block bg-maroon text-white font-bold py-3 px-8 rounded-lg hover:opacity-90 transition">
🚀 Mulai Analisis
</a>
</div>
@endif
@ -112,7 +151,9 @@
<script>
function toggleDetail(index) {
const detail = document.getElementById('detail-' + index);
detail.classList.toggle('hidden');
if (detail) {
detail.classList.toggle('hidden');
}
}
</script>
</body>

View File

@ -55,7 +55,7 @@
</header>
<!-- Main Content -->
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12 max-w-3xl">
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12 max-w-5xl">
{{-- Success Message --}}
@if (session('status') === 'profile-updated')
@ -64,30 +64,68 @@
</div>
@endif
{{-- ========== PROFILE HEADER CARD - HORIZONTAL ========== --}}
<div class="bg-gradient-to-r from-yellow-400 to-yellow-300 rounded-xl shadow-xl p-8 mb-8 text-gray-800">
<div class="flex gap-8 items-center">
<!-- Avatar Section -->
<div class="flex-shrink-0">
@if($user->foto)
<img src="{{ asset($user->foto) }}" alt="Foto Profil" class="w-40 h-40 rounded-2xl object-cover border-4 border-white shadow-lg" id="foto-preview-header">
@else
<div class="w-40 h-40 rounded-2xl bg-white bg-opacity-30 flex items-center justify-center text-6xl font-bold text-gray-700" id="foto-placeholder-header">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<img src="#" alt="Foto Profil" class="w-40 h-40 rounded-2xl object-cover border-4 border-white shadow-lg hidden" id="foto-preview-header">
@endif
</div>
<!-- Info Section -->
<div class="flex-1">
<h2 class="text-4xl font-bold mb-6">{{ $user->name }}</h2>
<div class="grid grid-cols-2 gap-6">
<div>
<p class="text-gray-700 text-sm font-semibold opacity-75">NIS</p>
<p class="text-2xl font-bold text-gray-800">{{ $user->nis ?? '-' }}</p>
</div>
<div>
<p class="text-gray-700 text-sm font-semibold opacity-75">Email</p>
<p class="text-lg font-semibold text-gray-800 break-all">{{ $user->email }}</p>
</div>
<div>
<p class="text-gray-700 text-sm font-semibold opacity-75">Kelompok</p>
@if($user->kelompok_asal)
<span class="inline-block px-4 py-2 rounded-full text-sm font-bold text-white" style="background-color: {{ $user->kelompok_asal == 'IPA' ? '#0369A1' : '#B45309' }};">
{{ $user->kelompok_asal }}
</span>
@else
<p class="text-lg font-bold">-</p>
@endif
</div>
<div>
<p class="text-gray-700 text-sm font-semibold opacity-75">Terdaftar</p>
<p class="text-lg font-semibold text-gray-800">{{ $user->created_at->format('d M Y') }}</p>
</div>
</div>
</div>
</div>
</div>
{{-- ========== INFORMASI PROFIL ========== --}}
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-blue-500">
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-1">Informasi Profil</h2>
<p class="text-xs sm:text-sm text-gray-500 mb-6">Perbarui data diri dan foto profil Anda.</p>
<div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 mb-6 border-l-4 border-blue-500">
<h2 class="text-xl font-bold text-maroon mb-1">📝 Edit Informasi Profil</h2>
<p class="text-sm text-gray-500 mb-6">Perbarui data diri dan foto profil Anda.</p>
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf
@method('patch')
{{-- Foto Profil --}}
{{-- Foto Profil Upload --}}
<div class="mb-6">
<label class="block text-sm font-semibold text-maroon mb-2">Foto Profil</label>
<div class="flex items-center gap-4">
@if($user->foto)
<img src="{{ asset($user->foto) }}" alt="Foto Profil" class="h-20 w-20 rounded-full object-cover border-2 border-maroon shadow" id="foto-preview">
@else
<div class="h-20 w-20 rounded-full bg-gray-200 flex items-center justify-center text-3xl border-2 border-maroon" id="foto-placeholder">
👤
</div>
<img src="#" alt="Foto Profil" class="h-20 w-20 rounded-full object-cover border-2 border-maroon shadow hidden" id="foto-preview">
@endif
<label class="block text-sm font-semibold text-maroon mb-3">🖼️ Foto Profil Baru</label>
<div class="flex items-center gap-6">
<div>
<input type="file" name="foto" id="foto" accept="image/*" class="text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-yellow-100 file:text-yellow-800 hover:file:bg-yellow-200" onchange="previewFoto(this)">
<p class="text-xs text-gray-400 mt-1">Format: JPG, PNG, GIF. Maks 2MB.</p>
<p class="text-xs text-gray-400 mt-2">Format: JPG, PNG, GIF. Maks 2MB.</p>
</div>
</div>
@error('foto')
@ -95,60 +133,62 @@
@enderror
</div>
{{-- Nama --}}
<div class="mb-4">
<label for="name" class="block text-sm font-semibold text-maroon mb-1">Nama Lengkap</label>
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
@error('name')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{-- Nama --}}
<div>
<label for="name" class="block text-sm font-semibold text-maroon mb-2">Nama Lengkap</label>
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
@error('name')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
{{-- Email --}}
<div>
<label for="email" class="block text-sm font-semibold text-maroon mb-2">Email</label>
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}" required
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
@error('email')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
{{-- NIS --}}
<div>
<label for="nis" class="block text-sm font-semibold text-maroon mb-2">NIS (Nomor Induk Siswa)</label>
<input type="text" id="nis" name="nis" value="{{ old('nis', $user->nis) }}" maxlength="20"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" placeholder="Contoh: 123456">
@error('nis')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
{{-- Kelompok Asal --}}
<div>
<label for="kelompok_asal" class="block text-sm font-semibold text-maroon mb-2">Kelompok Asal</label>
<select id="kelompok_asal" name="kelompok_asal"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0 bg-white">
<option value="">-- Pilih Kelompok --</option>
<option value="IPA" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPA' ? 'selected' : '' }}>IPA</option>
<option value="IPS" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPS' ? 'selected' : '' }}>IPS</option>
</select>
@error('kelompok_asal')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
</div>
{{-- Email --}}
<div class="mb-4">
<label for="email" class="block text-sm font-semibold text-maroon mb-1">Email</label>
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}" required
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
@error('email')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
{{-- NIS --}}
<div class="mb-4">
<label for="nis" class="block text-sm font-semibold text-maroon mb-1">NIS (Nomor Induk Siswa)</label>
<input type="text" id="nis" name="nis" value="{{ old('nis', $user->nis) }}" maxlength="20"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" placeholder="Contoh: 123456">
@error('nis')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
{{-- Kelompok Asal --}}
<div class="mb-6">
<label for="kelompok_asal" class="block text-sm font-semibold text-maroon mb-1">Kelompok Asal</label>
<select id="kelompok_asal" name="kelompok_asal"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0 bg-white">
<option value="">-- Pilih Kelompok --</option>
<option value="IPA" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPA' ? 'selected' : '' }}>IPA</option>
<option value="IPS" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPS' ? 'selected' : '' }}>IPS</option>
</select>
@error('kelompok_asal')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="btn-maroon font-bold py-2 px-6 rounded-lg text-sm">
Simpan Perubahan
<button type="submit" class="btn-maroon font-bold py-2 px-8 rounded-lg text-sm mt-6">
💾 Simpan Perubahan
</button>
</form>
</div>
{{-- ========== UBAH PASSWORD ========== --}}
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-green-500">
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-1">Ubah Password</h2>
<p class="text-xs sm:text-sm text-gray-500 mb-6">Pastikan akun Anda menggunakan password yang kuat dan aman.</p>
<div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 mb-6 border-l-4 border-green-500">
<h2 class="text-xl font-bold text-maroon mb-1">🔐 Ubah Password</h2>
<p class="text-sm text-gray-500 mb-6">Pastikan akun Anda menggunakan password yang kuat dan aman.</p>
@if (session('status') === 'password-updated')
<div class="bg-green-50 border border-green-300 text-green-800 rounded-lg p-4 mb-4 text-sm">
@ -160,62 +200,64 @@ class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm fo
@csrf
@method('put')
<div class="mb-4">
<label for="current_password" class="block text-sm font-semibold text-maroon mb-1">Password Saat Ini</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="password" id="current_password" name="current_password"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('current_password', this)">👁️</button>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="current_password" class="block text-sm font-semibold text-maroon mb-2">Password Saat Ini</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="password" id="current_password" name="current_password"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('current_password', this)">👁️</button>
</div>
@error('current_password', 'updatePassword')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password" class="block text-sm font-semibold text-maroon mb-2">Password Baru</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="password" id="password" name="password"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password', this)">👁️</button>
</div>
@error('password', 'updatePassword')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="md:col-span-2">
<label for="password_confirmation" class="block text-sm font-semibold text-maroon mb-2">Konfirmasi Password Baru</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="password" id="password_confirmation" name="password_confirmation"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password_confirmation', this)">👁️</button>
</div>
@error('password_confirmation', 'updatePassword')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
@error('current_password', 'updatePassword')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label for="password" class="block text-sm font-semibold text-maroon mb-1">Password Baru</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="password" id="password" name="password"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password', this)">👁️</button>
</div>
@error('password', 'updatePassword')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-6">
<label for="password_confirmation" class="block text-sm font-semibold text-maroon mb-1">Konfirmasi Password Baru</label>
<div style="position: relative; display: flex; align-items: center;">
<input type="password" id="password_confirmation" name="password_confirmation"
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password_confirmation', this)">👁️</button>
</div>
@error('password_confirmation', 'updatePassword')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg text-sm transition">
Ubah Password
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-8 rounded-lg text-sm transition mt-6">
Ubah Password
</button>
</form>
</div>
{{-- ========== HAPUS AKUN ========== --}}
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-red-500">
<h2 class="text-lg sm:text-xl font-bold text-red-700 mb-1">Hapus Akun</h2>
<p class="text-xs sm:text-sm text-gray-500 mb-4">
<div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 border-l-4 border-red-500">
<h2 class="text-xl font-bold text-red-700 mb-1">⚠️ Hapus Akun</h2>
<p class="text-sm text-gray-500 mb-4">
Setelah akun dihapus, semua data dan riwayat Anda akan dihapus secara permanen. Pastikan Anda sudah menyimpan data yang diperlukan.
</p>
<button type="button" onclick="document.getElementById('delete-section').classList.toggle('hidden')" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg text-sm transition">
Hapus Akun Saya
🗑️ Hapus Akun Saya
</button>
{{-- Konfirmasi Hapus --}}
<div id="delete-section" class="hidden mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<p class="text-sm text-red-800 mb-4 font-semibold">Apakah Anda yakin? Masukkan password untuk konfirmasi:</p>
<p class="text-sm text-red-800 mb-4 font-semibold">⚠️ Apakah Anda yakin? Masukkan password untuk konfirmasi:</p>
<form method="POST" action="{{ route('profile.destroy') }}">
@csrf
@method('delete')
@ -253,11 +295,11 @@ function previewFoto(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
var preview = document.getElementById('foto-preview');
var placeholder = document.getElementById('foto-placeholder');
preview.src = e.target.result;
preview.classList.remove('hidden');
if (placeholder) placeholder.classList.add('hidden');
var previewHeader = document.getElementById('foto-preview-header');
var placeholderHeader = document.getElementById('foto-placeholder-header');
previewHeader.src = e.target.result;
previewHeader.classList.remove('hidden');
if (placeholderHeader) placeholderHeader.classList.add('hidden');
}
reader.readAsDataURL(input.files[0]);
}

View File

@ -31,7 +31,7 @@ public function test_admin_can_add_jurusan_data()
]);
$response->assertRedirect();
$this->assertDatabaseHas('polije_majors', ['nama_jurusan' => 'Informatika']);
$this->assertDatabaseHas('jurusan_polije', ['nama_jurusan' => 'Informatika']);
}
/**
@ -54,7 +54,7 @@ public function test_bk_can_add_jurusan_data()
]);
$response->assertRedirect();
$this->assertDatabaseHas('polije_majors', ['nama_jurusan' => 'Akuntansi']);
$this->assertDatabaseHas('jurusan_polije', ['nama_jurusan' => 'Akuntansi']);
}
/**