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) ->take(5)
->get(); ->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( return view('admin.dashboard', compact(
'totalSiswa', 'totalSiswa',
'totalRekomendasi', 'totalRekomendasi',
@ -55,7 +79,13 @@ public function dashboard()
'recentStudents', 'recentStudents',
'recentRecommendations', 'recentRecommendations',
'kelompokStats', 'kelompokStats',
'topMajors' 'topMajors',
'chartMajorNames',
'chartMajorCounts',
'chartKelompokNames',
'chartKelompokCounts',
'topMajorsChart',
'topMajorsCounts'
)); ));
} }
@ -124,7 +154,7 @@ public function jurusanCreate()
public function jurusanStore(Request $request) public function jurusanStore(Request $request)
{ {
$request->validate([ $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', 'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string', 'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string', 'preferensi_studi' => 'nullable|string',
@ -162,7 +192,7 @@ public function jurusanUpdate(Request $request, $id)
$jurusan = PolijeMajor::findOrFail($id); $jurusan = PolijeMajor::findOrFail($id);
$request->validate([ $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', 'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string', 'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string', 'preferensi_studi' => 'nullable|string',
@ -336,8 +366,8 @@ public function riwayatChatbot(Request $request)
if ($request->filled('search')) { if ($request->filled('search')) {
$search = $request->search; $search = $request->search;
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('prompt', 'like', "%{$search}%") $q->where('pertanyaan', 'like', "%{$search}%")
->orWhere('response', 'like', "%{$search}%") ->orWhere('jawaban', 'like', "%{$search}%")
->orWhereHas('user', function ($q2) use ($search) { ->orWhereHas('user', function ($q2) use ($search) {
$q2->where('name', 'like', "%{$search}%"); $q2->where('name', 'like', "%{$search}%");
}); });

View File

@ -47,6 +47,30 @@ public function dashboard()
->take(5) ->take(5)
->get(); ->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( return view('bk.dashboard', compact(
'totalSiswa', 'totalSiswa',
'totalRekomendasi', 'totalRekomendasi',
@ -55,7 +79,13 @@ public function dashboard()
'recentStudents', 'recentStudents',
'recentRecommendations', 'recentRecommendations',
'kelompokStats', 'kelompokStats',
'topMajors' 'topMajors',
'chartMajorNames',
'chartMajorCounts',
'chartKelompokNames',
'chartKelompokCounts',
'topMajorsChart',
'topMajorsCounts'
)); ));
} }
@ -148,8 +178,8 @@ public function riwayatChatbot(Request $request)
if ($request->filled('search')) { if ($request->filled('search')) {
$search = $request->search; $search = $request->search;
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('prompt', 'like', "%{$search}%") $q->where('pertanyaan', 'like', "%{$search}%")
->orWhere('response', 'like', "%{$search}%") ->orWhere('jawaban', 'like', "%{$search}%")
->orWhereHas('user', function ($q2) use ($search) { ->orWhereHas('user', function ($q2) use ($search) {
$q2->where('name', 'like', "%{$search}%"); $q2->where('name', 'like', "%{$search}%");
}); });
@ -181,7 +211,7 @@ public function jurusanCreate()
public function jurusanStore(Request $request) public function jurusanStore(Request $request)
{ {
$request->validate([ $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', 'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string', 'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string', 'preferensi_studi' => 'nullable|string',
@ -219,7 +249,7 @@ public function jurusanUpdate(Request $request, $id)
$jurusan = PolijeMajor::findOrFail($id); $jurusan = PolijeMajor::findOrFail($id);
$request->validate([ $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', 'deskripsi' => 'nullable|string|max:1000',
'keywords' => 'nullable|string', 'keywords' => 'nullable|string',
'preferensi_studi' => 'nullable|string', 'preferensi_studi' => 'nullable|string',

View File

@ -36,7 +36,7 @@ public function index(Request $request)
if ($sessionId) { if ($sessionId) {
// Lanjutkan sesi lama — ambil semua chat dari sesi ini // Lanjutkan sesi lama — ambil semua chat dari sesi ini
$chats = ChatHistory::where('user_id', $user->id) $chats = ChatHistory::where('user_id', $user->id)
->where('session_id', $sessionId) ->where('id_sesi', $sessionId)
->orderBy('created_at', 'asc') ->orderBy('created_at', 'asc')
->get(); ->get();
@ -140,14 +140,32 @@ public function send(Request $request)
// Panggil Gemini API dengan conversation history // Panggil Gemini API dengan conversation history
$response = $this->geminiService->chat($message, $context, $chatHistory); $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 // Simpan chat ke database dengan session_id dan recommendation_id
if ($user && isset($response['message'])) { if ($user) {
ChatHistory::create([ ChatHistory::create([
'user_id' => $user->id, 'user_id' => $user->id,
'session_id' => $sessionId, 'id_sesi' => $sessionId,
'recommendation_id' => $recommendationId, 'id_rekomendasi' => $recommendationId,
'prompt' => $message, 'pertanyaan' => $message,
'response' => $response['message'], 'jawaban' => $responseMessage,
]); ]);
} }
@ -167,8 +185,8 @@ public function historyChat()
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->get(); ->get();
// Kelompokkan per session_id // Kelompokkan per id_sesi
$sessions = $chatHistories->groupBy('session_id')->map(function ($chats, $sessionId) { $sessions = $chatHistories->groupBy('id_sesi')->map(function ($chats, $sessionId) {
$first = $chats->last(); // oldest in group (karena desc) $first = $chats->last(); // oldest in group (karena desc)
$last = $chats->first(); // newest in group $last = $chats->first(); // newest in group
$rec = $first->recommendation; $rec = $first->recommendation;
@ -287,12 +305,12 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
} }
// Cari chat history yang mengandung kata kunci serupa // 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); ->where('user_id', $currentUserId);
$query->where(function ($q) use ($keywords) { $query->where(function ($q) use ($keywords) {
foreach ($keywords as $keyword) { 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 // Scoring: hitung berapa keyword yang cocok
$scored = []; $scored = [];
foreach ($candidates as $chat) { foreach ($candidates as $chat) {
$promptLower = strtolower($chat->prompt); $promptLower = strtolower($chat->pertanyaan);
$matchCount = 0; $matchCount = 0;
foreach ($keywords as $kw) { foreach ($keywords as $kw) {
if (stripos($promptLower, $kw) !== false) { if (stripos($promptLower, $kw) !== false) {
@ -317,8 +335,8 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
$ratio = $matchCount / count($keywords); $ratio = $matchCount / count($keywords);
if ($ratio >= 0.4) { // minimal 40% keyword cocok if ($ratio >= 0.4) { // minimal 40% keyword cocok
$scored[] = [ $scored[] = [
'prompt' => $chat->prompt, 'prompt' => $chat->pertanyaan,
'response' => Str::limit($chat->response, 300), 'response' => Str::limit($chat->jawaban, 300),
'score' => $ratio, 'score' => $ratio,
]; ];
} }

View File

@ -9,16 +9,56 @@ class ChatHistory extends Model
{ {
use HasFactory; use HasFactory;
protected $table = 'chat_histories'; protected $table = 'riwayat_chat';
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'session_id', 'id_sesi',
'recommendation_id', 'id_rekomendasi',
'prompt', 'pertanyaan',
'response', '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() public function user()
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
@ -26,6 +66,6 @@ public function user()
public function recommendation() 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; use HasFactory;
protected $table = 'jurusan_polije';
protected $fillable = [ protected $fillable = [
'nama_jurusan', 'nama_jurusan',
'deskripsi', 'deskripsi',

View File

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

View File

@ -33,7 +33,7 @@ public function chat($message, $context = [], $chatHistory = [])
if (empty($this->apiKey)) { if (empty($this->apiKey)) {
return [ return [
'success' => false, '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. // Mode Python-only: chatbot wajib melalui backend Python.
if (!empty($this->backendUrl)) { if (empty($this->backendUrl)) {
$proxyResponse = $this->sendViaPythonBackend($payload); return [
if (($proxyResponse['success'] ?? false) === true) { 'success' => false,
return $proxyResponse; 'message' => 'Layanan chatbot belum siap digunakan saat ini. Silakan coba kembali beberapa saat lagi.',
} ];
Log::warning('Python Gemini backend failed, fallback to direct API', [
'error' => $proxyResponse['message'] ?? 'unknown',
]);
} }
// Try each model until one works $proxyResponse = $this->sendViaPythonBackend($payload);
foreach ($this->models as $model) { if (($proxyResponse['success'] ?? false) === true) {
$url = $this->baseUrl . $model . ':generateContent?key=' . $this->apiKey; return $proxyResponse;
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);
}
} }
// All models failed Log::error('Python Gemini backend failed in Python-only mode', [
Log::error('All Gemini models failed, using fallback'); 'error' => $proxyResponse['message'] ?? 'unknown',
return $this->getFallbackResponse($message, $context, $chatHistory); ]);
return [
'success' => false,
'message' => $proxyResponse['message'] ?? 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba lagi.',
];
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Gemini Service Exception', [ Log::error('Gemini Service Exception', [
@ -138,7 +112,10 @@ public function chat($message, $context = [], $chatHistory = [])
'line' => $e->getLine() '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()) { 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 [ return [
'success' => false, 'success' => false,
'message' => 'Python backend error HTTP ' . $response->status(), 'message' => $mappedMessage,
]; ];
} }
@ -177,7 +164,7 @@ protected function sendViaPythonBackend(array $payload): array
return [ return [
'success' => false, 'success' => false,
'message' => $data['message'] ?? 'Python backend response invalid', 'message' => $data['message'] ?? 'Maaf, layanan chatbot belum dapat memberikan jawaban saat ini.',
]; ];
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning('Python backend exception', [ Log::warning('Python backend exception', [
@ -186,7 +173,7 @@ protected function sendViaPythonBackend(array $payload): array
return [ return [
'success' => false, '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 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: def _extract_text(data: Dict[str, Any]) -> str:
return ( return (
data.get("candidates", [{}])[0] data.get("candidates", [{}])[0]
@ -65,13 +75,13 @@ def _load_majors_file() -> Dict[str, Any]:
except FileNotFoundError: except FileNotFoundError:
return { return {
"ok": False, "ok": False,
"message": f"File data jurusan tidak ditemukan: {MAJORS_FILE_PATH}", "message": "Data jurusan belum tersedia.",
"data": {"majors": []}, "data": {"majors": []},
} }
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
return { return {
"ok": False, "ok": False,
"message": f"Format JSON tidak valid: {exc}", "message": "Data jurusan tidak dapat dibaca.",
"data": {"majors": []}, "data": {"majors": []},
} }
@ -166,10 +176,16 @@ def chat() -> Any:
_log("[PY-BACKEND] POST /api/chat") _log("[PY-BACKEND] POST /api/chat")
if not _is_authorized(): if not _is_authorized():
_log("[PY-BACKEND] Unauthorized request") _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: 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 {} body = request.get_json(silent=True) or {}
payload = body.get("payload") payload = body.get("payload")
@ -177,7 +193,10 @@ def chat() -> Any:
if not isinstance(payload, dict) or not payload.get("contents"): if not isinstance(payload, dict) or not payload.get("contents"):
_log("[PY-BACKEND] Invalid payload") _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() majors_result = _load_majors_file()
if majors_result["ok"]: if majors_result["ok"]:
@ -207,19 +226,30 @@ def chat() -> Any:
if text: if text:
_log(f"[PY-BACKEND] Success using model: {model}") _log(f"[PY-BACKEND] Success using model: {model}")
return jsonify({"success": True, "message": text, "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 continue
if resp.status_code in (404, 429): if resp.status_code in (404, 429):
if resp.status_code == 429: if resp.status_code == 429:
time.sleep(1) 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 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}") _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__": 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>
</div> </div>
<!-- Kelompok Distribution --> <!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <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"> <!-- Rekomendasi Distribution Chart -->
<h3 class="text-lg font-bold text-maroon mb-4">📊 Siswa per Kelompok</h3> <div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
<div class="space-y-3"> <h3 class="text-lg font-bold text-maroon mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
@foreach($kelompokStats as $stat) <div style="position: relative; height: 300px;">
<div class="flex items-center gap-3"> <canvas id="chartRecommendations"></canvas>
<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
</div> </div>
</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"> <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> <h3 class="text-lg font-bold text-maroon mb-4">🎯 Top Recommended Majors</h3>
@if($topMajors->isNotEmpty()) <div style="position: relative; height: 250px;">
<div class="space-y-3"> <canvas id="chartTopMajors"></canvas>
@foreach($topMajors as $major) </div>
<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> </div>
</div> </div>
@ -135,3 +136,140 @@
@endif @endif
</div> </div>
@endsection @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 class="sidebar-brand-icon">🎓</div>
<div> <div>
<p class="text-white font-bold text-sm leading-tight">SPK Jurusan</p> <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> </div>
</div> </div>
@ -178,8 +178,8 @@
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }} {{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
</div> </div>
<div class="flex-1 min-w-0"> <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 font-medium text-white 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 text-white">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,120 +13,132 @@
</a> </a>
</div> </div>
<!-- Profile Card --> <!-- Profile Header Card - Horizontal Layout -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-maroon"> <div class="bg-gradient-to-r from-maroon to-teal-600 rounded-lg shadow-lg p-8 mb-6 text-white">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="flex gap-8 items-start">
<div> <!-- Avatar Section -->
<p class="text-gray-600 text-sm font-semibold">Nama</p> <div class="flex-shrink-0">
<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>
@if($student->foto) @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 @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 @endif
</div> </div>
<div>
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p> <!-- Info Section -->
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p> <div class="flex-1">
</div> <h3 class="text-3xl font-bold mb-4">{{ $student->name }}</h3>
</div>
</div> <div class="grid grid-cols-2 gap-4 md:gap-6">
<div>
<!-- Rekomendasi --> <p class="text-white text-opacity-80 text-sm font-semibold">NIS</p>
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400"> <p class="text-lg font-bold">{{ $student->nis ?? '-' }}</p>
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3> </div>
<div>
@if($recommendations->isNotEmpty()) <p class="text-white text-opacity-80 text-sm font-semibold">Kelompok</p>
<div class="space-y-4"> @if($student->kelompok_asal)
@foreach($recommendations as $rec) <span class="inline-block px-3 py-1 rounded-full text-sm font-bold bg-white text-maroon mt-1">
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition"> {{ $student->kelompok_asal }}
<div class="flex justify-between items-start mb-3"> </span>
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p> @else
<span class="px-2 py-1 rounded text-xs font-bold bg-blue-100 text-blue-800">Rekomendasi #{{ $loop->index + 1 }}</span> <p class="text-lg font-bold">-</p>
</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>
@endif @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> </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> </div>
@else </div>
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
@endif
</div> </div>
<!-- Chat History --> <!-- Main Content Grid -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-maroon">💬 Chat History ({{ count($chatHistories) }})</h3> <!-- Left Column: Rekomendasi (2/3 width) -->
@if(count($chatHistories) > 0) <div class="lg:col-span-2">
<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"> <div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
Lihat Semua <h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
</a>
@endif @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> </div>
@if($chatHistories->isNotEmpty()) <!-- Right Column: Chat History (1/3 width) -->
<div class="space-y-3 max-h-96 overflow-y-auto"> <div class="lg:col-span-1">
@foreach($chatHistories as $chat) <div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
<div class="border-b pb-3 last:border-b-0"> <div class="flex justify-between items-center mb-4">
<div class="flex justify-between items-start mb-2"> <h3 class="text-lg font-bold text-maroon">💬 Chat</h3>
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p> <span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
</div> </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> @if($chatHistories->isNotEmpty())
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p> <div class="space-y-3 max-h-96 overflow-y-auto pr-2">
</div> @foreach($chatHistories->take(5) as $chat)
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded"> <div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p> <p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p> <p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
</div> <p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
</div>
@endforeach
</div> </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> </div>
@else </div>
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
@endif
</div> </div>
@endsection @endsection

View File

@ -28,39 +28,41 @@
</div> </div>
</div> </div>
<!-- Kelompok Distribution & Top Majors --> <!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <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"> <!-- Rekomendasi Distribution Chart -->
<h3 class="text-lg font-bold text-bk mb-4">📊 Siswa per Kelompok</h3> <div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
<div class="space-y-3"> <h3 class="text-lg font-bold text-bk mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
@foreach($kelompokStats as $stat) <div style="position: relative; height: 300px;">
<div class="flex items-center gap-3"> <canvas id="chartRecommendations"></canvas>
<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
</div> </div>
</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"> <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> <h3 class="text-lg font-bold text-bk mb-4">🎯 Jurusan Terpopuler</h3>
@if($topMajors->isNotEmpty()) <div style="position: relative; height: 250px;">
<div class="space-y-3"> <canvas id="chartTopMajors"></canvas>
@foreach($topMajors as $major) </div>
<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> </div>
</div> </div>
@ -139,3 +141,140 @@
@endif @endif
</div> </div>
@endsection @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> </a>
</div> </div>
<!-- Profile Card --> <!-- Profile Header Card - Horizontal Layout -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-teal-500"> <div class="bg-gradient-to-r from-teal-600 to-teal-400 rounded-lg shadow-lg p-8 mb-6 text-white">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="flex gap-8 items-start">
<div> <!-- Avatar Section -->
<p class="text-gray-600 text-sm font-semibold">Nama</p> <div class="flex-shrink-0">
<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>
@if($student->foto) @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 @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 @endif
</div> </div>
<div>
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p> <!-- Info Section -->
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p> <div class="flex-1">
</div> <h3 class="text-3xl font-bold mb-4">{{ $student->name }}</h3>
</div>
</div> <div class="grid grid-cols-2 gap-4 md:gap-6">
<div>
<!-- Rekomendasi --> <p class="text-white text-opacity-80 text-sm font-semibold">NIS</p>
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400"> <p class="text-lg font-bold">{{ $student->nis ?? '-' }}</p>
<h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3> </div>
<div>
@if($recommendations->isNotEmpty()) <p class="text-white text-opacity-80 text-sm font-semibold">Kelompok</p>
<div class="space-y-4"> @if($student->kelompok_asal)
@foreach($recommendations as $rec) <span class="inline-block px-3 py-1 rounded-full text-sm font-bold bg-white text-teal-600 mt-1">
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition"> {{ $student->kelompok_asal }}
<div class="flex justify-between items-start mb-3"> </span>
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p> @else
<span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-800">Rekomendasi #{{ $loop->index + 1 }}</span> <p class="text-lg font-bold">-</p>
</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>
@endif @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> </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> </div>
@else </div>
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
@endif
</div> </div>
<!-- Chat History --> <!-- Main Content Grid -->
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-bk">💬 Chat History ({{ count($chatHistories) }})</h3> <!-- Left Column: Rekomendasi (2/3 width) -->
@if(count($chatHistories) > 0) <div class="lg:col-span-2">
<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"> <div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
Lihat Semua <h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
</a>
@endif @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> </div>
@if($chatHistories->isNotEmpty()) <!-- Right Column: Chat History (1/3 width) -->
<div class="space-y-3 max-h-96 overflow-y-auto"> <div class="lg:col-span-1">
@foreach($chatHistories as $chat) <div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
<div class="border-b pb-3 last:border-b-0"> <div class="flex justify-between items-center mb-4">
<div class="flex justify-between items-start mb-2"> <h3 class="text-lg font-bold text-bk">💬 Chat</h3>
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p> <span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
</div> </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> @if($chatHistories->isNotEmpty())
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p> <div class="space-y-3 max-h-96 overflow-y-auto pr-2">
</div> @foreach($chatHistories->take(5) as $chat)
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded"> <div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p> <p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p> <p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
</div> <p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
</div>
@endforeach
</div> </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> </div>
@else </div>
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
@endif
</div> </div>
@endsection @endsection

View File

@ -25,85 +25,124 @@
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50"> <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 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> <div>
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">History Rekomendasi</h1> <h1 class="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> <p class="text-sm text-yellow-200 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>
</div> </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> </div>
</header> </header>
<!-- Main Content --> <!-- 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) @if($recommendations && $recommendations->count() > 0)
<div class="space-y-4 sm:space-y-6"> <div class="space-y-6">
@foreach($recommendations as $rec) @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="bg-white rounded-lg shadow-lg overflow-hidden 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"> <!-- Header Card -->
<div> <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 }})">
<h3 class="text-lg sm:text-xl font-bold text-maroon mb-2"> <div class="flex justify-between items-start gap-4">
Analisis - {{ \Carbon\Carbon::parse($rec->created_at)->format('d M Y H:i') }} <div class="flex-1">
</h3> <h3 class="text-2xl font-bold mb-2">📅 {{ \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> <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> </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>
<div id="detail-{{ $loop->index }}" class="hidden"> <!-- Detail Card (Hidden by default) -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mb-4 sm:mb-6"> <div id="detail-{{ $loop->index }}" class="hidden border-t-4 border-yellow-200 p-6">
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg"> <!-- Input Data Summary -->
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Minat</p> <div class="mb-8">
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->minat ?? '-' }}</p> <h4 class="text-lg font-bold text-maroon mb-4">📝 Data Input</h4>
</div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg"> <div class="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500">
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Pref. Belajar</p> <p class="text-xs text-blue-700 font-semibold mb-1">💭 Minat</p>
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->preferensi_studi ?? '-' }}</p> <p class="text-sm font-bold text-gray-800">{{ $rec->minat ?? '-' }}</p>
</div> </div>
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg"> <div class="bg-purple-50 p-4 rounded-lg border-l-4 border-purple-500">
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Cita-Cita</p> <p class="text-xs text-purple-700 font-semibold mb-1">🎓 Preferensi Studi</p>
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->cita_cita ?? '-', 15) }}</p> <p class="text-sm font-bold text-gray-800">{{ $rec->preferensi_studi ?? '-' }}</p>
</div> </div>
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg"> <div class="bg-green-50 p-4 rounded-lg border-l-4 border-green-500">
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Prestasi</p> <p class="text-xs text-green-700 font-semibold mb-1">🎯 Cita-Cita</p>
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->prestasi ?? '-', 15) }}</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> </div>
<div class="bg-yellow-50 p-4 sm:p-6 rounded-lg"> <!-- Nilai Akademik -->
<h4 class="font-bold text-maroon mb-3 sm:mb-4">Top 3 Rekomendasi Jurusan</h4> <div class="mb-8">
<div class="space-y-2 sm:space-y-3"> <h4 class="text-lg font-bold text-maroon mb-4">📚 Nilai Akademik</h4>
@if($rec->hasil_rekomendasi) <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) @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 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-3 sm:gap-4"> <div class="flex items-center gap-4 flex-1">
<span class="text-lg sm:text-xl font-bold text-yellow-500">{{ $index + 1 }}</span> <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">
<span class="text-sm sm:text-base font-bold text-maroon">{{ $hasil['jurusan'] ?? '-' }}</span> {{ $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> </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) }}% {{ number_format(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
</span> </span>
</div> </div>
@endforeach @endforeach
@endif </div>
</div> @endif
</div> </div>
</div> </div>
</div> </div>
@endforeach @endforeach
</div> </div>
@else @else
<div class="bg-white rounded-lg shadow-lg p-8 sm:p-12 text-center"> <div class="bg-white rounded-lg shadow-lg p-12 text-center">
<div class="text-5xl sm:text-6xl mb-4">📊</div> <div class="text-6xl mb-4">📊</div>
<h3 class="text-xl sm:text-2xl font-bold text-maroon mb-2">Belum Ada History</h3> <h3 class="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> <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-2 sm:py-3 px-6 sm:px-8 rounded-lg hover:opacity-90 transition text-sm sm:text-base"> <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 🚀 Mulai Analisis
</a> </a>
</div> </div>
@endif @endif
@ -112,7 +151,9 @@
<script> <script>
function toggleDetail(index) { function toggleDetail(index) {
const detail = document.getElementById('detail-' + index); const detail = document.getElementById('detail-' + index);
detail.classList.toggle('hidden'); if (detail) {
detail.classList.toggle('hidden');
}
} }
</script> </script>
</body> </body>

View File

@ -55,7 +55,7 @@
</header> </header>
<!-- Main Content --> <!-- 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 --}} {{-- Success Message --}}
@if (session('status') === 'profile-updated') @if (session('status') === 'profile-updated')
@ -64,30 +64,68 @@
</div> </div>
@endif @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 ========== --}} {{-- ========== INFORMASI PROFIL ========== --}}
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-blue-500"> <div class="bg-white rounded-lg shadow-lg p-6 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> <h2 class="text-xl font-bold text-maroon mb-1">📝 Edit Informasi Profil</h2>
<p class="text-xs sm:text-sm text-gray-500 mb-6">Perbarui data diri dan foto profil Anda.</p> <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"> <form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf @csrf
@method('patch') @method('patch')
{{-- Foto Profil --}} {{-- Foto Profil Upload --}}
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-semibold text-maroon mb-2">Foto Profil</label> <label class="block text-sm font-semibold text-maroon mb-3">🖼️ Foto Profil Baru</label>
<div class="flex items-center gap-4"> <div class="flex items-center gap-6">
@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
<div> <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)"> <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>
</div> </div>
@error('foto') @error('foto')
@ -95,60 +133,62 @@
@enderror @enderror
</div> </div>
{{-- Nama --}} <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="mb-4"> {{-- Nama --}}
<label for="name" class="block text-sm font-semibold text-maroon mb-1">Nama Lengkap</label> <div>
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required <label for="name" class="block text-sm font-semibold text-maroon mb-2">Nama Lengkap</label>
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0"> <input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required
@error('name') class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
<p class="text-red-600 text-xs mt-1">{{ $message }}</p> @error('name')
@enderror <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> </div>
{{-- Email --}} <button type="submit" class="btn-maroon font-bold py-2 px-8 rounded-lg text-sm mt-6">
<div class="mb-4"> 💾 Simpan Perubahan
<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> </button>
</form> </form>
</div> </div>
{{-- ========== UBAH PASSWORD ========== --}} {{-- ========== UBAH PASSWORD ========== --}}
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-green-500"> <div class="bg-white rounded-lg shadow-lg p-6 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> <h2 class="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> <p class="text-sm text-gray-500 mb-6">Pastikan akun Anda menggunakan password yang kuat dan aman.</p>
@if (session('status') === 'password-updated') @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"> <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 @csrf
@method('put') @method('put')
<div class="mb-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<label for="current_password" class="block text-sm font-semibold text-maroon mb-1">Password Saat Ini</label> <div>
<div style="position: relative; display: flex; align-items: center;"> <label for="current_password" class="block text-sm font-semibold text-maroon mb-2">Password Saat Ini</label>
<input type="password" id="current_password" name="current_password" <div style="position: relative; display: flex; align-items: center;">
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;"> <input type="password" id="current_password" name="current_password"
<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> 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> </div>
@error('current_password', 'updatePassword')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div> </div>
<div class="mb-4"> <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">
<label for="password" class="block text-sm font-semibold text-maroon mb-1">Password Baru</label> Ubah Password
<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> </button>
</form> </form>
</div> </div>
{{-- ========== HAPUS AKUN ========== --}} {{-- ========== HAPUS AKUN ========== --}}
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-red-500"> <div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 border-l-4 border-red-500">
<h2 class="text-lg sm:text-xl font-bold text-red-700 mb-1">Hapus Akun</h2> <h2 class="text-xl font-bold text-red-700 mb-1">⚠️ Hapus Akun</h2>
<p class="text-xs sm:text-sm text-gray-500 mb-4"> <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. Setelah akun dihapus, semua data dan riwayat Anda akan dihapus secara permanen. Pastikan Anda sudah menyimpan data yang diperlukan.
</p> </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"> <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> </button>
{{-- Konfirmasi Hapus --}} {{-- Konfirmasi Hapus --}}
<div id="delete-section" class="hidden mt-6 p-4 bg-red-50 border border-red-200 rounded-lg"> <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') }}"> <form method="POST" action="{{ route('profile.destroy') }}">
@csrf @csrf
@method('delete') @method('delete')
@ -253,11 +295,11 @@ function previewFoto(input) {
if (input.files && input.files[0]) { if (input.files && input.files[0]) {
var reader = new FileReader(); var reader = new FileReader();
reader.onload = function(e) { reader.onload = function(e) {
var preview = document.getElementById('foto-preview'); var previewHeader = document.getElementById('foto-preview-header');
var placeholder = document.getElementById('foto-placeholder'); var placeholderHeader = document.getElementById('foto-placeholder-header');
preview.src = e.target.result; previewHeader.src = e.target.result;
preview.classList.remove('hidden'); previewHeader.classList.remove('hidden');
if (placeholder) placeholder.classList.add('hidden'); if (placeholderHeader) placeholderHeader.classList.add('hidden');
} }
reader.readAsDataURL(input.files[0]); reader.readAsDataURL(input.files[0]);
} }

View File

@ -31,7 +31,7 @@ public function test_admin_can_add_jurusan_data()
]); ]);
$response->assertRedirect(); $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(); $response->assertRedirect();
$this->assertDatabaseHas('polije_majors', ['nama_jurusan' => 'Akuntansi']); $this->assertDatabaseHas('jurusan_polije', ['nama_jurusan' => 'Akuntansi']);
} }
/** /**