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:
parent
3f0ce730a4
commit
b48f27505e
|
|
@ -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}}
|
||||
|
|
@ -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!
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -47,6 +47,30 @@ public function dashboard()
|
|||
->take(5)
|
||||
->get();
|
||||
|
||||
// Data untuk chart - semua jurusan
|
||||
$allMajorsChart = Recommendation::selectRaw("
|
||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||
COUNT(*) as count
|
||||
")
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
|
||||
->orderBy('count', 'desc')
|
||||
->get();
|
||||
|
||||
// Persiapkan data untuk Chart.js
|
||||
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
|
||||
return trim($name, '"');
|
||||
})->toArray();
|
||||
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
|
||||
|
||||
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
|
||||
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
||||
|
||||
// Top majors untuk horizontal bar chart
|
||||
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
|
||||
return trim($name, '"');
|
||||
})->toArray();
|
||||
$topMajorsCounts = $topMajors->pluck('count')->toArray();
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'totalSiswa',
|
||||
'totalRekomendasi',
|
||||
|
|
@ -55,7 +79,13 @@ public function dashboard()
|
|||
'recentStudents',
|
||||
'recentRecommendations',
|
||||
'kelompokStats',
|
||||
'topMajors'
|
||||
'topMajors',
|
||||
'chartMajorNames',
|
||||
'chartMajorCounts',
|
||||
'chartKelompokNames',
|
||||
'chartKelompokCounts',
|
||||
'topMajorsChart',
|
||||
'topMajorsCounts'
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +154,7 @@ public function jurusanCreate()
|
|||
public function jurusanStore(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'nama_jurusan' => 'required|string|max:255|unique:polije_majors,nama_jurusan',
|
||||
'nama_jurusan' => 'required|string|max:255|unique:jurusan_polije,nama_jurusan',
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
|
|
@ -162,7 +192,7 @@ public function jurusanUpdate(Request $request, $id)
|
|||
$jurusan = PolijeMajor::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('polije_majors', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('jurusan_polije', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
|
|
@ -336,8 +366,8 @@ public function riwayatChatbot(Request $request)
|
|||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('prompt', 'like', "%{$search}%")
|
||||
->orWhere('response', 'like', "%{$search}%")
|
||||
$q->where('pertanyaan', 'like', "%{$search}%")
|
||||
->orWhere('jawaban', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($q2) use ($search) {
|
||||
$q2->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,6 +47,30 @@ public function dashboard()
|
|||
->take(5)
|
||||
->get();
|
||||
|
||||
// Data untuk chart - semua jurusan
|
||||
$allMajorsChart = Recommendation::selectRaw("
|
||||
JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan') as major_name,
|
||||
COUNT(*) as count
|
||||
")
|
||||
->groupByRaw("JSON_EXTRACT(hasil_rekomendasi, '$[0].jurusan')")
|
||||
->orderBy('count', 'desc')
|
||||
->get();
|
||||
|
||||
// Persiapkan data untuk Chart.js
|
||||
$chartMajorNames = $allMajorsChart->pluck('major_name')->map(function($name) {
|
||||
return trim($name, '"');
|
||||
})->toArray();
|
||||
$chartMajorCounts = $allMajorsChart->pluck('count')->toArray();
|
||||
|
||||
$chartKelompokNames = $kelompokStats->pluck('kelompok_asal')->toArray();
|
||||
$chartKelompokCounts = $kelompokStats->pluck('count')->toArray();
|
||||
|
||||
// Top majors untuk horizontal bar chart
|
||||
$topMajorsChart = $topMajors->pluck('major_name')->map(function($name) {
|
||||
return trim($name, '"');
|
||||
})->toArray();
|
||||
$topMajorsCounts = $topMajors->pluck('count')->toArray();
|
||||
|
||||
return view('bk.dashboard', compact(
|
||||
'totalSiswa',
|
||||
'totalRekomendasi',
|
||||
|
|
@ -55,7 +79,13 @@ public function dashboard()
|
|||
'recentStudents',
|
||||
'recentRecommendations',
|
||||
'kelompokStats',
|
||||
'topMajors'
|
||||
'topMajors',
|
||||
'chartMajorNames',
|
||||
'chartMajorCounts',
|
||||
'chartKelompokNames',
|
||||
'chartKelompokCounts',
|
||||
'topMajorsChart',
|
||||
'topMajorsCounts'
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +178,8 @@ public function riwayatChatbot(Request $request)
|
|||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('prompt', 'like', "%{$search}%")
|
||||
->orWhere('response', 'like', "%{$search}%")
|
||||
$q->where('pertanyaan', 'like', "%{$search}%")
|
||||
->orWhere('jawaban', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($q2) use ($search) {
|
||||
$q2->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
|
|
@ -181,7 +211,7 @@ public function jurusanCreate()
|
|||
public function jurusanStore(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'nama_jurusan' => 'required|string|max:255|unique:polije_majors,nama_jurusan',
|
||||
'nama_jurusan' => 'required|string|max:255|unique:jurusan_polije,nama_jurusan',
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
|
|
@ -219,7 +249,7 @@ public function jurusanUpdate(Request $request, $id)
|
|||
$jurusan = PolijeMajor::findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('polije_majors', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'nama_jurusan' => ['required', 'string', 'max:255', Rule::unique('jurusan_polije', 'nama_jurusan')->ignore($jurusan->id)],
|
||||
'deskripsi' => 'nullable|string|max:1000',
|
||||
'keywords' => 'nullable|string',
|
||||
'preferensi_studi' => 'nullable|string',
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public function index(Request $request)
|
|||
if ($sessionId) {
|
||||
// Lanjutkan sesi lama — ambil semua chat dari sesi ini
|
||||
$chats = ChatHistory::where('user_id', $user->id)
|
||||
->where('session_id', $sessionId)
|
||||
->where('id_sesi', $sessionId)
|
||||
->orderBy('created_at', 'asc')
|
||||
->get();
|
||||
|
||||
|
|
@ -140,14 +140,32 @@ public function send(Request $request)
|
|||
// Panggil Gemini API dengan conversation history
|
||||
$response = $this->geminiService->chat($message, $context, $chatHistory);
|
||||
|
||||
// Normalisasi respons agar error tetap memiliki pesan yang konsisten.
|
||||
$isSuccess = (bool) ($response['success'] ?? false);
|
||||
$errorCode = (string) ($response['error_code'] ?? 'CHAT_SERVICE_ERROR');
|
||||
$responseMessage = trim((string) ($response['message'] ?? ''));
|
||||
|
||||
if ($responseMessage === '') {
|
||||
$responseMessage = $isSuccess
|
||||
? 'Respons berhasil diproses.'
|
||||
: 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.';
|
||||
}
|
||||
|
||||
if (!$isSuccess) {
|
||||
$responseMessage = "[ERROR:{$errorCode}] {$responseMessage}";
|
||||
}
|
||||
|
||||
$response['message'] = $responseMessage;
|
||||
$response['error_code'] = $isSuccess ? null : $errorCode;
|
||||
|
||||
// Simpan chat ke database dengan session_id dan recommendation_id
|
||||
if ($user && isset($response['message'])) {
|
||||
if ($user) {
|
||||
ChatHistory::create([
|
||||
'user_id' => $user->id,
|
||||
'session_id' => $sessionId,
|
||||
'recommendation_id' => $recommendationId,
|
||||
'prompt' => $message,
|
||||
'response' => $response['message'],
|
||||
'id_sesi' => $sessionId,
|
||||
'id_rekomendasi' => $recommendationId,
|
||||
'pertanyaan' => $message,
|
||||
'jawaban' => $responseMessage,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -167,8 +185,8 @@ public function historyChat()
|
|||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
// Kelompokkan per session_id
|
||||
$sessions = $chatHistories->groupBy('session_id')->map(function ($chats, $sessionId) {
|
||||
// Kelompokkan per id_sesi
|
||||
$sessions = $chatHistories->groupBy('id_sesi')->map(function ($chats, $sessionId) {
|
||||
$first = $chats->last(); // oldest in group (karena desc)
|
||||
$last = $chats->first(); // newest in group
|
||||
$rec = $first->recommendation;
|
||||
|
|
@ -287,12 +305,12 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
|
|||
}
|
||||
|
||||
// Cari chat history yang mengandung kata kunci serupa
|
||||
$query = ChatHistory::select('prompt', 'response', 'created_at')
|
||||
$query = ChatHistory::select('pertanyaan', 'jawaban', 'created_at')
|
||||
->where('user_id', $currentUserId);
|
||||
|
||||
$query->where(function ($q) use ($keywords) {
|
||||
foreach ($keywords as $keyword) {
|
||||
$q->orWhere('prompt', 'like', "%{$keyword}%");
|
||||
$q->orWhere('pertanyaan', 'like', "%{$keyword}%");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -307,7 +325,7 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
|
|||
// Scoring: hitung berapa keyword yang cocok
|
||||
$scored = [];
|
||||
foreach ($candidates as $chat) {
|
||||
$promptLower = strtolower($chat->prompt);
|
||||
$promptLower = strtolower($chat->pertanyaan);
|
||||
$matchCount = 0;
|
||||
foreach ($keywords as $kw) {
|
||||
if (stripos($promptLower, $kw) !== false) {
|
||||
|
|
@ -317,8 +335,8 @@ private function findSimilarQuestions(string $message, int $currentUserId): arra
|
|||
$ratio = $matchCount / count($keywords);
|
||||
if ($ratio >= 0.4) { // minimal 40% keyword cocok
|
||||
$scored[] = [
|
||||
'prompt' => $chat->prompt,
|
||||
'response' => Str::limit($chat->response, 300),
|
||||
'prompt' => $chat->pertanyaan,
|
||||
'response' => Str::limit($chat->jawaban, 300),
|
||||
'score' => $ratio,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,56 @@ class ChatHistory extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'chat_histories';
|
||||
protected $table = 'riwayat_chat';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'session_id',
|
||||
'recommendation_id',
|
||||
'prompt',
|
||||
'response',
|
||||
'id_sesi',
|
||||
'id_rekomendasi',
|
||||
'pertanyaan',
|
||||
'jawaban',
|
||||
];
|
||||
|
||||
public function getSessionIdAttribute()
|
||||
{
|
||||
return $this->attributes['id_sesi'] ?? null;
|
||||
}
|
||||
|
||||
public function setSessionIdAttribute($value): void
|
||||
{
|
||||
$this->attributes['id_sesi'] = $value;
|
||||
}
|
||||
|
||||
public function getPromptAttribute()
|
||||
{
|
||||
return $this->attributes['pertanyaan'] ?? null;
|
||||
}
|
||||
|
||||
public function setPromptAttribute($value): void
|
||||
{
|
||||
$this->attributes['pertanyaan'] = $value;
|
||||
}
|
||||
|
||||
public function getResponseAttribute()
|
||||
{
|
||||
return $this->attributes['jawaban'] ?? null;
|
||||
}
|
||||
|
||||
public function setResponseAttribute($value): void
|
||||
{
|
||||
$this->attributes['jawaban'] = $value;
|
||||
}
|
||||
|
||||
public function getRecommendationIdAttribute()
|
||||
{
|
||||
return $this->attributes['id_rekomendasi'] ?? null;
|
||||
}
|
||||
|
||||
public function setRecommendationIdAttribute($value): void
|
||||
{
|
||||
$this->attributes['id_rekomendasi'] = $value;
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
|
@ -26,6 +66,6 @@ public function user()
|
|||
|
||||
public function recommendation()
|
||||
{
|
||||
return $this->belongsTo(Recommendation::class);
|
||||
return $this->belongsTo(Recommendation::class, 'id_rekomendasi');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ class PolijeMajor extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'jurusan_polije';
|
||||
|
||||
protected $fillable = [
|
||||
'nama_jurusan',
|
||||
'deskripsi',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ class Recommendation extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'rekomendasi';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'mtk',
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
if (empty($this->apiKey)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'API Key tidak tersedia. Silakan konfigurasi GEMINI_API_KEY di .env'
|
||||
'message' => 'Layanan chatbot belum siap digunakan. Silakan hubungi pengelola sistem.'
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -83,53 +83,27 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
]
|
||||
];
|
||||
|
||||
// Jika backend Python dikonfigurasi, gunakan sebagai gateway Gemini terlebih dahulu.
|
||||
if (!empty($this->backendUrl)) {
|
||||
$proxyResponse = $this->sendViaPythonBackend($payload);
|
||||
if (($proxyResponse['success'] ?? false) === true) {
|
||||
return $proxyResponse;
|
||||
}
|
||||
|
||||
Log::warning('Python Gemini backend failed, fallback to direct API', [
|
||||
'error' => $proxyResponse['message'] ?? 'unknown',
|
||||
]);
|
||||
// Mode Python-only: chatbot wajib melalui backend Python.
|
||||
if (empty($this->backendUrl)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Layanan chatbot belum siap digunakan saat ini. Silakan coba kembali beberapa saat lagi.',
|
||||
];
|
||||
}
|
||||
|
||||
// Try each model until one works
|
||||
foreach ($this->models as $model) {
|
||||
$url = $this->baseUrl . $model . ':generateContent?key=' . $this->apiKey;
|
||||
|
||||
Log::info('Trying Gemini model', ['model' => $model]);
|
||||
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders(['Content-Type' => 'application/json'])
|
||||
->post($url, $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
|
||||
if (isset($data['candidates'][0]['content']['parts'][0]['text'])) {
|
||||
Log::info('Gemini API success', ['model' => $model]);
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $data['candidates'][0]['content']['parts'][0]['text']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// If 429 (rate limit) or 404 (model not found), try next model
|
||||
$status = $response->status();
|
||||
Log::warning("Gemini model {$model} failed", ['status' => $status]);
|
||||
|
||||
if ($status === 429) {
|
||||
// Wait briefly before trying next model
|
||||
sleep(1);
|
||||
}
|
||||
$proxyResponse = $this->sendViaPythonBackend($payload);
|
||||
if (($proxyResponse['success'] ?? false) === true) {
|
||||
return $proxyResponse;
|
||||
}
|
||||
|
||||
// All models failed
|
||||
Log::error('All Gemini models failed, using fallback');
|
||||
return $this->getFallbackResponse($message, $context, $chatHistory);
|
||||
Log::error('Python Gemini backend failed in Python-only mode', [
|
||||
'error' => $proxyResponse['message'] ?? 'unknown',
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $proxyResponse['message'] ?? 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba lagi.',
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Gemini Service Exception', [
|
||||
|
|
@ -138,7 +112,10 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
'line' => $e->getLine()
|
||||
]);
|
||||
|
||||
return $this->getFallbackResponse($message, $context, $chatHistory);
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Maaf, layanan chatbot sedang mengalami kendala. Silakan coba kembali beberapa saat lagi.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,9 +138,19 @@ protected function sendViaPythonBackend(array $payload): array
|
|||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
$status = $response->status();
|
||||
|
||||
$mappedMessage = match ($status) {
|
||||
401, 403 => 'Maaf, layanan chatbot sedang dibatasi sementara. Silakan coba kembali nanti.',
|
||||
422 => 'Pesan belum dapat diproses. Silakan perjelas pertanyaan Anda dan coba lagi.',
|
||||
429 => 'Layanan chatbot sedang ramai digunakan. Silakan tunggu sebentar lalu coba lagi.',
|
||||
500, 502, 503, 504 => 'Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.',
|
||||
default => 'Layanan chatbot sedang tidak tersedia. Silakan coba kembali nanti.',
|
||||
};
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Python backend error HTTP ' . $response->status(),
|
||||
'message' => $mappedMessage,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +164,7 @@ protected function sendViaPythonBackend(array $payload): array
|
|||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $data['message'] ?? 'Python backend response invalid',
|
||||
'message' => $data['message'] ?? 'Maaf, layanan chatbot belum dapat memberikan jawaban saat ini.',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Python backend exception', [
|
||||
|
|
@ -186,7 +173,7 @@ protected function sendViaPythonBackend(array $payload): array
|
|||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Python backend exception',
|
||||
'message' => 'Koneksi layanan chatbot sedang bermasalah. Silakan coba kembali beberapa saat lagi.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
|
@ -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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,16 @@ def _is_authorized() -> bool:
|
|||
return token == BACKEND_TOKEN
|
||||
|
||||
|
||||
def _http_error(message: str, status_code: int, detail: str = "") -> Any:
|
||||
payload = {
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
if detail:
|
||||
payload["error"] = detail
|
||||
return jsonify(payload), status_code
|
||||
|
||||
|
||||
def _extract_text(data: Dict[str, Any]) -> str:
|
||||
return (
|
||||
data.get("candidates", [{}])[0]
|
||||
|
|
@ -65,13 +75,13 @@ def _load_majors_file() -> Dict[str, Any]:
|
|||
except FileNotFoundError:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": f"File data jurusan tidak ditemukan: {MAJORS_FILE_PATH}",
|
||||
"message": "Data jurusan belum tersedia.",
|
||||
"data": {"majors": []},
|
||||
}
|
||||
except json.JSONDecodeError as exc:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": f"Format JSON tidak valid: {exc}",
|
||||
"message": "Data jurusan tidak dapat dibaca.",
|
||||
"data": {"majors": []},
|
||||
}
|
||||
|
||||
|
|
@ -166,10 +176,16 @@ def chat() -> Any:
|
|||
_log("[PY-BACKEND] POST /api/chat")
|
||||
if not _is_authorized():
|
||||
_log("[PY-BACKEND] Unauthorized request")
|
||||
return jsonify({"success": False, "message": "Unauthorized"}), 401
|
||||
return _http_error(
|
||||
"Akses ke layanan chatbot ditolak.",
|
||||
401,
|
||||
)
|
||||
|
||||
if not GEMINI_API_KEY:
|
||||
return jsonify({"success": False, "message": "GEMINI_API_KEY belum diset di backend Python"}), 500
|
||||
return _http_error(
|
||||
"Layanan chatbot belum siap digunakan.",
|
||||
500,
|
||||
)
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
payload = body.get("payload")
|
||||
|
|
@ -177,7 +193,10 @@ def chat() -> Any:
|
|||
|
||||
if not isinstance(payload, dict) or not payload.get("contents"):
|
||||
_log("[PY-BACKEND] Invalid payload")
|
||||
return jsonify({"success": False, "message": "payload tidak valid"}), 422
|
||||
return _http_error(
|
||||
"Pesan belum dapat diproses. Silakan perjelas pertanyaan Anda.",
|
||||
422,
|
||||
)
|
||||
|
||||
majors_result = _load_majors_file()
|
||||
if majors_result["ok"]:
|
||||
|
|
@ -207,19 +226,30 @@ def chat() -> Any:
|
|||
if text:
|
||||
_log(f"[PY-BACKEND] Success using model: {model}")
|
||||
return jsonify({"success": True, "message": text, "model": model})
|
||||
last_error = "Respons model tidak berisi teks"
|
||||
last_error = "Jawaban dari layanan tidak ditemukan."
|
||||
continue
|
||||
|
||||
if resp.status_code in (404, 429):
|
||||
if resp.status_code == 429:
|
||||
time.sleep(1)
|
||||
last_error = f"Model {model} gagal dengan status {resp.status_code}"
|
||||
if resp.status_code == 429:
|
||||
last_error = (
|
||||
"Layanan chatbot sedang ramai digunakan. Silakan tunggu sebentar lalu coba lagi."
|
||||
)
|
||||
else:
|
||||
last_error = "Layanan chatbot sementara tidak tersedia."
|
||||
continue
|
||||
|
||||
last_error = f"Gemini error {resp.status_code}: {resp.text[:300]}"
|
||||
last_error = (
|
||||
"Terjadi gangguan pada layanan chatbot."
|
||||
)
|
||||
|
||||
_log(f"[PY-BACKEND] Failed all models: {last_error}")
|
||||
return jsonify({"success": False, "message": "Semua model gagal", "error": last_error}), 502
|
||||
return _http_error(
|
||||
"Maaf, layanan chatbot sedang mengalami gangguan. Silakan coba kembali beberapa saat lagi.",
|
||||
502,
|
||||
last_error,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
2026-04-23 00:18:45,831 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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 [33mPress CTRL+C to quit[0m
|
||||
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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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 [33mPress CTRL+C to quit[0m
|
||||
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 [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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 [33mPress CTRL+C to quit[0m
|
||||
2026-04-27 07:40:08,145 INFO * Restarting with stat
|
||||
2026-04-27 07:41:46,436 INFO [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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 [33mPress CTRL+C to quit[0m
|
||||
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
|
||||
|
|
@ -23,40 +23,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution -->
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Siswa per Kelompok</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($kelompokStats as $stat)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
|
||||
<div class="flex-1 h-6 bg-gray-200 rounded">
|
||||
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
|
||||
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
|
||||
{{ $stat->count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<!-- Rekomendasi Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartRecommendations"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Recommended Majors -->
|
||||
<!-- Kelompok Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-indigo-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📈 Distribusi Siswa per Kelompok</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartKelompok"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Kelompok Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-maroon">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">📊 Siswa per Kelompok</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartKelompokPie"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Recommended Majors Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Top Recommended Majors</h3>
|
||||
@if($topMajors->isNotEmpty())
|
||||
<div class="space-y-3">
|
||||
@foreach($topMajors as $major)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 flex-1 truncate">{{ $major->major_name }}</span>
|
||||
<span class="px-3 py-1 rounded bg-green-100 text-green-800 font-bold text-sm">{{ $major->count }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Belum ada data rekomendasi</p>
|
||||
@endif
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartTopMajors"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -135,3 +136,140 @@
|
|||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<script>
|
||||
// Chart 1: Rekomendasi Distribution
|
||||
const chartRecommendationsCtx = document.getElementById('chartRecommendations').getContext('2d');
|
||||
const chartRecommendations = new Chart(chartRecommendationsCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: @json($chartMajorNames),
|
||||
datasets: [{
|
||||
data: @json($chartMajorCounts),
|
||||
backgroundColor: [
|
||||
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
||||
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0'
|
||||
],
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: { size: 11 },
|
||||
padding: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 2: Kelompok Distribution
|
||||
const chartKelompokCtx = document.getElementById('chartKelompok').getContext('2d');
|
||||
const chartKelompok = new Chart(chartKelompokCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($chartKelompokNames),
|
||||
datasets: [{
|
||||
label: 'Jumlah Siswa',
|
||||
data: @json($chartKelompokCounts),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: ['#0369A1', '#D97706'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 3: Kelompok Pie Chart
|
||||
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
|
||||
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: @json($chartKelompokNames),
|
||||
datasets: [{
|
||||
data: @json($chartKelompokCounts),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: { size: 11 },
|
||||
padding: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 4: Top Majors Horizontal Bar Chart
|
||||
const chartTopMajorsCtx = document.getElementById('chartTopMajors').getContext('2d');
|
||||
const chartTopMajors = new Chart(chartTopMajorsCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($topMajorsChart),
|
||||
datasets: [{
|
||||
label: 'Jumlah Rekomendasi',
|
||||
data: @json($topMajorsCounts),
|
||||
backgroundColor: '#36A2EB',
|
||||
borderColor: '#36A2EB',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
<div class="sidebar-brand-icon">🎓</div>
|
||||
<div>
|
||||
<p class="text-white font-bold text-sm leading-tight">SPK Jurusan</p>
|
||||
<p class="text-xs text-slate-400">Admin Panel</p>
|
||||
<p class="text-xs text-white">Admin Panel</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -178,8 +178,8 @@
|
|||
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium text-slate-300 truncate">{{ Auth::user()->name }}</p>
|
||||
<p class="text-xs text-slate-500">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
|
||||
<p class="text-xs font-medium text-white truncate">{{ Auth::user()->name }}</p>
|
||||
<p class="text-xs text-white">{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,120 +13,132 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-maroon">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Nama</p>
|
||||
<p class="text-xl font-bold text-maroon mt-1">{{ $student->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Email</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">NIS</p>
|
||||
<p class="text-gray-800 font-semibold mt-1">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Kelompok Asal</p>
|
||||
@if($student->kelompok_asal)
|
||||
<p class="mt-1">
|
||||
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
</p>
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Foto Profil</p>
|
||||
<!-- Profile Header Card - Horizontal Layout -->
|
||||
<div class="bg-gradient-to-r from-maroon to-teal-600 rounded-lg shadow-lg p-8 mb-6 text-white">
|
||||
<div class="flex gap-8 items-start">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex-shrink-0">
|
||||
@if($student->foto)
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 rounded-lg object-cover mt-2 border-2 border-maroon">
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-32 h-32 rounded-xl object-cover border-4 border-white shadow-lg">
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
<div class="w-32 h-32 rounded-xl bg-white bg-opacity-20 flex items-center justify-center text-3xl font-bold">
|
||||
{{ strtoupper(substr($student->name, 0, 1)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rekomendasi -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded text-xs font-bold bg-blue-100 text-blue-800">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="mt-3">
|
||||
<p class="text-xs font-semibold text-gray-600 mb-2">Top 3 Rekomendasi:</p>
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#DBEAFE' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#1e40af' : '#6B7280' }};">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="flex-1">
|
||||
<h3 class="text-3xl font-bold mb-4">{{ $student->name }}</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 md:gap-6">
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">NIS</p>
|
||||
<p class="text-lg font-bold">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">Kelompok</p>
|
||||
@if($student->kelompok_asal)
|
||||
<span class="inline-block px-3 py-1 rounded-full text-sm font-bold bg-white text-maroon mt-1">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
@else
|
||||
<p class="text-lg font-bold">-</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 p-3 bg-gray-50 rounded text-xs text-gray-600">
|
||||
<p><strong>Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">Email</p>
|
||||
<p class="text-sm font-semibold break-all">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">Terdaftar</p>
|
||||
<p class="text-sm font-semibold">{{ $student->created_at->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat History -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-maroon">💬 Chat History ({{ count($chatHistories) }})</h3>
|
||||
@if(count($chatHistories) > 0)
|
||||
<a href="{{ route('admin.student.chat', $student->id) }}" class="bg-blue-500 text-white font-semibold py-2 px-3 rounded-lg hover:bg-blue-600 transition text-xs">
|
||||
Lihat Semua →
|
||||
</a>
|
||||
@endif
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Left Column: Rekomendasi (2/3 width) -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-maroon mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border-2 border-green-100 rounded-lg p-4 hover:shadow-md hover:border-green-400 transition">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<p class="text-xs font-semibold text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between bg-gradient-to-r {{ $idx === 0 ? 'from-green-50 to-transparent' : 'from-gray-50 to-transparent' }} p-3 rounded">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full {{ $idx === 0 ? 'bg-green-500 text-white' : 'bg-gray-300 text-white' }} text-xs font-bold">
|
||||
{{ $idx + 1 }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-800">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
</div>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-3 py-1 rounded-full text-xs font-bold {{ $idx === 0 ? 'bg-green-200 text-green-800' : 'bg-gray-200 text-gray-800' }}">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(isset($rec->minat) || isset($rec->cita_cita))
|
||||
<div class="mt-3 p-3 bg-blue-50 border-l-4 border-blue-300 rounded">
|
||||
<p class="text-xs text-gray-700"><strong>📝 Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>🎓 Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<p class="text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto">
|
||||
@foreach($chatHistories as $chat)
|
||||
<div class="border-b pb-3 last:border-b-0">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-3 rounded mb-2">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">👤 Pertanyaan Siswa:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p>
|
||||
</div>
|
||||
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p>
|
||||
</div>
|
||||
<!-- Right Column: Chat History (1/3 width) -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-maroon">💬 Chat</h3>
|
||||
<span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
|
||||
@foreach($chatHistories->take(5) as $chat)
|
||||
<div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
|
||||
<p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
|
||||
<p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
|
||||
<p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
@if(count($chatHistories) > 5)
|
||||
<a href="{{ route('admin.student.chat', $student->id) }}" class="block mt-3 text-center text-xs text-blue-600 font-semibold hover:text-blue-800">
|
||||
Lihat Semua ({{ count($chatHistories) }}) →
|
||||
</a>
|
||||
@endif
|
||||
@else
|
||||
<p class="text-gray-500 text-xs text-center py-4">Belum ada chat</p>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -28,39 +28,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution & Top Majors -->
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-teal-500">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📊 Siswa per Kelompok</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($kelompokStats as $stat)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 w-20">{{ $stat->kelompok_asal ?? 'Tidak Ada' }}</span>
|
||||
<div class="flex-1 h-6 bg-gray-200 rounded">
|
||||
<div class="h-full rounded flex items-center justify-center text-white text-xs font-bold"
|
||||
style="width: {{ $totalSiswa > 0 ? ($stat->count / $totalSiswa) * 100 : 0 }}%; background-color: {{ $stat->kelompok_asal == 'IPA' ? '#0369A1' : '#D97706' }};">
|
||||
{{ $stat->count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<!-- Rekomendasi Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-purple-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📊 Distribusi Rekomendasi per Jurusan</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartRecommendations"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-indigo-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📈 Distribusi Siswa per Kelompok</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="chartKelompok"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kelompok Distribution & Top Majors -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Kelompok Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-teal-500">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">📊 Siswa per Kelompok</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartKelompokPie"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Recommended Majors Chart -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">🎯 Jurusan Terpopuler</h3>
|
||||
@if($topMajors->isNotEmpty())
|
||||
<div class="space-y-3">
|
||||
@foreach($topMajors as $major)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 flex-1 truncate">{{ $major->major_name }}</span>
|
||||
<span class="px-3 py-1 rounded bg-green-100 text-green-800 font-bold text-sm">{{ $major->count }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Belum ada data rekomendasi</p>
|
||||
@endif
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="chartTopMajors"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -139,3 +141,140 @@
|
|||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<script>
|
||||
// Chart 1: Rekomendasi Distribution
|
||||
const chartRecommendationsCtx = document.getElementById('chartRecommendations').getContext('2d');
|
||||
const chartRecommendations = new Chart(chartRecommendationsCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: @json($chartMajorNames),
|
||||
datasets: [{
|
||||
data: @json($chartMajorCounts),
|
||||
backgroundColor: [
|
||||
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
||||
'#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0'
|
||||
],
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: { size: 11 },
|
||||
padding: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 2: Kelompok Distribution Bar
|
||||
const chartKelompokCtx = document.getElementById('chartKelompok').getContext('2d');
|
||||
const chartKelompok = new Chart(chartKelompokCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($chartKelompokNames),
|
||||
datasets: [{
|
||||
label: 'Jumlah Siswa',
|
||||
data: @json($chartKelompokCounts),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: ['#0369A1', '#D97706'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 3: Kelompok Pie Chart
|
||||
const chartKelompokPieCtx = document.getElementById('chartKelompokPie').getContext('2d');
|
||||
const chartKelompokPie = new Chart(chartKelompokPieCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: @json($chartKelompokNames),
|
||||
datasets: [{
|
||||
data: @json($chartKelompokCounts),
|
||||
backgroundColor: ['#0369A1', '#D97706'],
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
font: { size: 11 },
|
||||
padding: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart 4: Top Majors Horizontal Bar Chart
|
||||
const chartTopMajorsCtx = document.getElementById('chartTopMajors').getContext('2d');
|
||||
const chartTopMajors = new Chart(chartTopMajorsCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($topMajorsChart),
|
||||
datasets: [{
|
||||
label: 'Jumlah Rekomendasi',
|
||||
data: @json($topMajorsCounts),
|
||||
backgroundColor: '#36A2EB',
|
||||
borderColor: '#36A2EB',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
font: { size: 11 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,120 +13,132 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6 border-l-4 border-teal-500">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Nama</p>
|
||||
<p class="text-xl font-bold text-bk mt-1">{{ $student->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Email</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">NIS</p>
|
||||
<p class="text-gray-800 font-semibold mt-1">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Kelompok Asal</p>
|
||||
@if($student->kelompok_asal)
|
||||
<p class="mt-1">
|
||||
<span class="px-3 py-1 rounded text-sm font-bold" style="{{ $student->kelompok_asal == 'IPA' ? 'background-color: #E0F2FE; color: #0369A1;' : 'background-color: #FEF3C7; color: #92400E;' }}">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
</p>
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Foto Profil</p>
|
||||
<!-- Profile Header Card - Horizontal Layout -->
|
||||
<div class="bg-gradient-to-r from-teal-600 to-teal-400 rounded-lg shadow-lg p-8 mb-6 text-white">
|
||||
<div class="flex gap-8 items-start">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex-shrink-0">
|
||||
@if($student->foto)
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-24 h-24 rounded-lg object-cover mt-2 border-2 border-teal-500">
|
||||
<img src="{{ Storage::url($student->foto) }}" alt="{{ $student->name }}" class="w-32 h-32 rounded-xl object-cover border-4 border-white shadow-lg">
|
||||
@else
|
||||
<p class="text-gray-500 mt-1">-</p>
|
||||
<div class="w-32 h-32 rounded-xl bg-white bg-opacity-20 flex items-center justify-center text-3xl font-bold">
|
||||
{{ strtoupper(substr($student->name, 0, 1)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-600 text-sm font-semibold">Terdaftar</p>
|
||||
<p class="text-gray-800 mt-1">{{ $student->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rekomendasi -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<p class="text-sm text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-800">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="mt-3">
|
||||
<p class="text-xs font-semibold text-gray-600 mb-2">Top 3 Rekomendasi:</p>
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700">{{ $idx + 1 }}. {{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-2 py-1 rounded text-xs font-bold" style="background-color: {{ $idx === 0 ? '#CCFBF1' : '#F3F4F6' }}; color: {{ $idx === 0 ? '#0f766e' : '#6B7280' }};">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="flex-1">
|
||||
<h3 class="text-3xl font-bold mb-4">{{ $student->name }}</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 md:gap-6">
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">NIS</p>
|
||||
<p class="text-lg font-bold">{{ $student->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">Kelompok</p>
|
||||
@if($student->kelompok_asal)
|
||||
<span class="inline-block px-3 py-1 rounded-full text-sm font-bold bg-white text-teal-600 mt-1">
|
||||
{{ $student->kelompok_asal }}
|
||||
</span>
|
||||
@else
|
||||
<p class="text-lg font-bold">-</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 p-3 bg-gray-50 rounded text-xs text-gray-600">
|
||||
<p><strong>Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">Email</p>
|
||||
<p class="text-sm font-semibold break-all">{{ $student->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white text-opacity-80 text-sm font-semibold">Terdaftar</p>
|
||||
<p class="text-sm font-semibold">{{ $student->created_at->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat History -->
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-bk">💬 Chat History ({{ count($chatHistories) }})</h3>
|
||||
@if(count($chatHistories) > 0)
|
||||
<a href="{{ route('bk.student.chat', $student->id) }}" class="bg-teal-500 text-white font-semibold py-2 px-3 rounded-lg hover:bg-teal-600 transition text-xs">
|
||||
Lihat Semua →
|
||||
</a>
|
||||
@endif
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Left Column: Rekomendasi (2/3 width) -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-green-400">
|
||||
<h3 class="text-lg font-bold text-bk mb-4">🎯 Riwayat Rekomendasi ({{ count($recommendations) }})</h3>
|
||||
|
||||
@if($recommendations->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="border-2 border-green-100 rounded-lg p-4 hover:shadow-md hover:border-green-400 transition">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<p class="text-xs font-semibold text-gray-500">{{ $rec->created_at->format('d M Y H:i') }}</p>
|
||||
<span class="px-2 py-1 rounded-full text-xs font-bold bg-teal-100 text-teal-700">Rekomendasi #{{ $loop->index + 1 }}</span>
|
||||
</div>
|
||||
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="space-y-2">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $idx => $hasil)
|
||||
<div class="flex items-center justify-between bg-gradient-to-r {{ $idx === 0 ? 'from-teal-50 to-transparent' : 'from-gray-50 to-transparent' }} p-3 rounded">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full {{ $idx === 0 ? 'bg-teal-500 text-white' : 'bg-gray-300 text-white' }} text-xs font-bold">
|
||||
{{ $idx + 1 }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-800">{{ $hasil['jurusan'] ?? 'N/A' }}</span>
|
||||
</div>
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-3 py-1 rounded-full text-xs font-bold {{ $idx === 0 ? 'bg-teal-200 text-teal-800' : 'bg-gray-200 text-gray-800' }}">
|
||||
{{ round(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if(isset($rec->minat) || isset($rec->cita_cita))
|
||||
<div class="mt-3 p-3 bg-blue-50 border-l-4 border-blue-300 rounded">
|
||||
<p class="text-xs text-gray-700"><strong>📝 Minat:</strong> {{ $rec->minat ?? '-' }} | <strong>🎓 Cita-cita:</strong> {{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<p class="text-sm">Siswa belum melakukan rekomendasi</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto">
|
||||
@foreach($chatHistories as $chat)
|
||||
<div class="border-b pb-3 last:border-b-0">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="text-xs font-semibold text-gray-600">{{ $chat->created_at->format('d M Y H:i') }}</p>
|
||||
</div>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-3 rounded mb-2">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">👤 Pertanyaan Siswa:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}</p>
|
||||
</div>
|
||||
<div class="bg-green-50 border-l-4 border-green-400 p-3 rounded">
|
||||
<p class="text-xs font-semibold text-gray-700 mb-1">🤖 Jawaban AI:</p>
|
||||
<p class="text-sm text-gray-800">{{ \Illuminate\Support\Str::limit($chat->response, 150) }}</p>
|
||||
</div>
|
||||
<!-- Right Column: Chat History (1/3 width) -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow p-6 border-l-4 border-blue-400 sticky top-20">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-bk">💬 Chat</h3>
|
||||
<span class="text-xs font-bold bg-blue-100 text-blue-700 px-2 py-1 rounded-full">{{ count($chatHistories) }}</span>
|
||||
</div>
|
||||
|
||||
@if($chatHistories->isNotEmpty())
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
|
||||
@foreach($chatHistories->take(5) as $chat)
|
||||
<div class="border border-blue-100 rounded-lg p-3 hover:bg-blue-50 transition">
|
||||
<p class="text-xs font-semibold text-gray-500 mb-2">{{ $chat->created_at->format('d M Y') }}</p>
|
||||
<p class="text-xs text-gray-700 mb-2"><strong>Q:</strong> {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}</p>
|
||||
<p class="text-xs text-gray-600"><strong>A:</strong> {{ \Illuminate\Support\Str::limit($chat->response, 80) }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
@if(count($chatHistories) > 5)
|
||||
<a href="{{ route('bk.student.chat', $student->id) }}" class="block mt-3 text-center text-xs text-teal-600 font-semibold hover:text-teal-800">
|
||||
Lihat Semua ({{ count($chatHistories) }}) →
|
||||
</a>
|
||||
@endif
|
||||
@else
|
||||
<p class="text-gray-500 text-xs text-center py-4">Belum ada chat</p>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-sm">Siswa belum melakukan chat dengan AI</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -25,85 +25,124 @@
|
|||
<header class="gradient-maroon text-white shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-4 sm:py-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl md:text-3xl font-bold">History Rekomendasi</h1>
|
||||
<p class="text-xs sm:text-sm text-yellow-300 font-semibold mt-1">Semua Analisis Anda</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||
<a href="{{ url('/dashboard') }}" class="block sm:inline-block flex-1 sm:flex-none text-center bg-yellow-400 text-maroon font-bold py-2 px-3 sm:px-4 rounded-lg hover:bg-yellow-300 transition text-xs sm:text-sm">
|
||||
Kembali Dashboard
|
||||
</a>
|
||||
<h1 class="text-2xl md:text-3xl font-bold">📊 History Rekomendasi</h1>
|
||||
<p class="text-sm text-yellow-200 font-semibold mt-1">Semua Analisis Anda</p>
|
||||
</div>
|
||||
<a href="{{ url('/dashboard') }}" class="bg-yellow-400 text-maroon font-bold py-2 px-4 rounded-lg hover:bg-yellow-300 transition text-sm">
|
||||
← Kembali Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||
@if($recommendations && $recommendations->count() > 0)
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
<div class="space-y-6">
|
||||
@foreach($recommendations as $rec)
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-6 border-l-4 border-maroon hover:shadow-xl transition">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4 mb-4 sm:mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg sm:text-xl font-bold text-maroon mb-2">
|
||||
Analisis - {{ \Carbon\Carbon::parse($rec->created_at)->format('d M Y H:i') }}
|
||||
</h3>
|
||||
<p class="text-xs sm:text-sm text-gray-600">{{ $rec->created_at->diffForHumans() }}</p>
|
||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden border-l-4 border-maroon hover:shadow-xl transition">
|
||||
<!-- Header Card -->
|
||||
<div class="bg-gradient-to-r from-yellow-400 to-yellow-300 p-6 text-gray-800 cursor-pointer hover:bg-gradient-to-r hover:from-yellow-300 hover:to-yellow-200 transition" onclick="toggleDetail({{ $loop->index }})">
|
||||
<div class="flex justify-between items-start gap-4">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-2xl font-bold mb-2">📅 {{ \Carbon\Carbon::parse($rec->created_at)->format('d M Y H:i') }}</h3>
|
||||
<p class="text-sm text-gray-700 font-semibold">{{ $rec->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-white text-maroon font-bold text-sm">
|
||||
Lihat Detail ↓
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="toggleDetail({{ $loop->index }})" class="w-full sm:w-auto bg-maroon text-white font-bold py-2 px-4 rounded-lg hover:opacity-90 transition text-xs sm:text-sm">
|
||||
Lihat Detail
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="detail-{{ $loop->index }}" class="hidden">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mb-4 sm:mb-6">
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Minat</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->minat ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Pref. Belajar</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ $rec->preferensi_studi ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Cita-Cita</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->cita_cita ?? '-', 15) }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg">
|
||||
<p class="text-xs sm:text-sm text-gray-600 font-semibold">Prestasi</p>
|
||||
<p class="text-sm sm:text-base font-bold text-maroon mt-1">{{ Str::limit($rec->prestasi ?? '-', 15) }}</p>
|
||||
<!-- Detail Card (Hidden by default) -->
|
||||
<div id="detail-{{ $loop->index }}" class="hidden border-t-4 border-yellow-200 p-6">
|
||||
<!-- Input Data Summary -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-lg font-bold text-maroon mb-4">📝 Data Input</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500">
|
||||
<p class="text-xs text-blue-700 font-semibold mb-1">💭 Minat</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->minat ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-purple-50 p-4 rounded-lg border-l-4 border-purple-500">
|
||||
<p class="text-xs text-purple-700 font-semibold mb-1">🎓 Preferensi Studi</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->preferensi_studi ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-green-50 p-4 rounded-lg border-l-4 border-green-500">
|
||||
<p class="text-xs text-green-700 font-semibold mb-1">🎯 Cita-Cita</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->cita_cita ?? '-' }}</p>
|
||||
</div>
|
||||
<div class="bg-red-50 p-4 rounded-lg border-l-4 border-red-500">
|
||||
<p class="text-xs text-red-700 font-semibold mb-1">🏆 Prestasi</p>
|
||||
<p class="text-sm font-bold text-gray-800">{{ $rec->prestasi ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 p-4 sm:p-6 rounded-lg">
|
||||
<h4 class="font-bold text-maroon mb-3 sm:mb-4">Top 3 Rekomendasi Jurusan</h4>
|
||||
<div class="space-y-2 sm:space-y-3">
|
||||
@if($rec->hasil_rekomendasi)
|
||||
<!-- Nilai Akademik -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-lg font-bold text-maroon mb-4">📚 Nilai Akademik</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
@php
|
||||
$subjects = [
|
||||
['name' => 'Matematika', 'key' => 'mtk', 'icon' => '🔢'],
|
||||
['name' => 'Fisika', 'key' => 'fisika', 'icon' => '⚡'],
|
||||
['name' => 'Kimia', 'key' => 'kimia', 'icon' => '🧪'],
|
||||
['name' => 'Biologi', 'key' => 'biologi', 'icon' => '🔬'],
|
||||
['name' => 'Ekonomi', 'key' => 'ekonomi', 'icon' => '💰'],
|
||||
['name' => 'Geografi', 'key' => 'geografi', 'icon' => '🌍'],
|
||||
['name' => 'Sosiologi', 'key' => 'sosiologi', 'icon' => '👥'],
|
||||
['name' => 'Sejarah', 'key' => 'sejarah', 'icon' => '📜'],
|
||||
];
|
||||
@endphp
|
||||
@foreach($subjects as $subject)
|
||||
@if($rec->{$subject['key']} !== null)
|
||||
<div class="bg-gray-50 p-3 rounded-lg border border-gray-200 hover:border-maroon transition">
|
||||
<p class="text-xs text-gray-600 font-semibold mb-1">{{ $subject['icon'] }} {{ $subject['name'] }}</p>
|
||||
<p class="text-lg font-bold text-maroon">{{ $rec->{$subject['key']} }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top 3 Rekomendasi Jurusan -->
|
||||
<div>
|
||||
<h4 class="text-lg font-bold text-maroon mb-4">🎯 Top 3 Rekomendasi Jurusan</h4>
|
||||
@if($rec->hasil_rekomendasi && is_array($rec->hasil_rekomendasi))
|
||||
<div class="space-y-3">
|
||||
@foreach(array_slice($rec->hasil_rekomendasi, 0, 3) as $index => $hasil)
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-4 p-3 sm:p-4 bg-white rounded-lg border border-yellow-300">
|
||||
<div class="flex items-center gap-3 sm:gap-4">
|
||||
<span class="text-lg sm:text-xl font-bold text-yellow-500">{{ $index + 1 }}</span>
|
||||
<span class="text-sm sm:text-base font-bold text-maroon">{{ $hasil['jurusan'] ?? '-' }}</span>
|
||||
<div class="flex items-center justify-between p-4 bg-gradient-to-r {{ $index === 0 ? 'from-yellow-50 to-yellow-100' : ($index === 1 ? 'from-gray-50 to-gray-100' : 'from-orange-50 to-orange-100') }} rounded-lg border-l-4 {{ $index === 0 ? 'border-yellow-500' : ($index === 1 ? 'border-gray-500' : 'border-orange-500') }}">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full {{ $index === 0 ? 'bg-yellow-500 text-white' : ($index === 1 ? 'bg-gray-500 text-white' : 'bg-orange-500 text-white') }} font-bold text-lg">
|
||||
{{ $index + 1 }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-bold text-gray-600">Rekomendasi {{ $index + 1 }}</p>
|
||||
<p class="text-lg font-bold text-gray-800">{{ $hasil['jurusan'] ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs sm:text-sm bg-maroon text-white px-3 py-1 rounded-full font-bold">
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
@php $skorVal = $hasil['skor'] ?? 0; @endphp
|
||||
<span class="px-4 py-2 rounded-full font-bold text-white {{ $index === 0 ? 'bg-yellow-500' : ($index === 1 ? 'bg-gray-500' : 'bg-orange-500') }}">
|
||||
{{ number_format(($skorVal > 1 ? $skorVal : $skorVal * 100), 1) }}%
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white rounded-lg shadow-lg p-8 sm:p-12 text-center">
|
||||
<div class="text-5xl sm:text-6xl mb-4">📊</div>
|
||||
<h3 class="text-xl sm:text-2xl font-bold text-maroon mb-2">Belum Ada History</h3>
|
||||
<p class="text-xs sm:text-sm md:text-base text-gray-700 mb-6">Anda belum melakukan analisis rekomendasi. Mulai sekarang!</p>
|
||||
<a href="{{ url('/rekomendasi') }}" class="inline-block bg-maroon text-white font-bold py-2 sm:py-3 px-6 sm:px-8 rounded-lg hover:opacity-90 transition text-sm sm:text-base">
|
||||
Mulai Analisis
|
||||
<div class="bg-white rounded-lg shadow-lg p-12 text-center">
|
||||
<div class="text-6xl mb-4">📊</div>
|
||||
<h3 class="text-2xl font-bold text-maroon mb-2">Belum Ada History</h3>
|
||||
<p class="text-gray-700 mb-6">Anda belum melakukan analisis rekomendasi. Mulai sekarang untuk melihat history!</p>
|
||||
<a href="{{ url('/rekomendasi') }}" class="inline-block bg-maroon text-white font-bold py-3 px-8 rounded-lg hover:opacity-90 transition">
|
||||
🚀 Mulai Analisis
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -112,7 +151,9 @@
|
|||
<script>
|
||||
function toggleDetail(index) {
|
||||
const detail = document.getElementById('detail-' + index);
|
||||
detail.classList.toggle('hidden');
|
||||
if (detail) {
|
||||
detail.classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12 max-w-3xl">
|
||||
<div class="container mx-auto px-4 sm:px-6 py-6 sm:py-12 max-w-5xl">
|
||||
|
||||
{{-- Success Message --}}
|
||||
@if (session('status') === 'profile-updated')
|
||||
|
|
@ -64,30 +64,68 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ========== PROFILE HEADER CARD - HORIZONTAL ========== --}}
|
||||
<div class="bg-gradient-to-r from-yellow-400 to-yellow-300 rounded-xl shadow-xl p-8 mb-8 text-gray-800">
|
||||
<div class="flex gap-8 items-center">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex-shrink-0">
|
||||
@if($user->foto)
|
||||
<img src="{{ asset($user->foto) }}" alt="Foto Profil" class="w-40 h-40 rounded-2xl object-cover border-4 border-white shadow-lg" id="foto-preview-header">
|
||||
@else
|
||||
<div class="w-40 h-40 rounded-2xl bg-white bg-opacity-30 flex items-center justify-center text-6xl font-bold text-gray-700" id="foto-placeholder-header">
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
</div>
|
||||
<img src="#" alt="Foto Profil" class="w-40 h-40 rounded-2xl object-cover border-4 border-white shadow-lg hidden" id="foto-preview-header">
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Info Section -->
|
||||
<div class="flex-1">
|
||||
<h2 class="text-4xl font-bold mb-6">{{ $user->name }}</h2>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="text-gray-700 text-sm font-semibold opacity-75">NIS</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ $user->nis ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 text-sm font-semibold opacity-75">Email</p>
|
||||
<p class="text-lg font-semibold text-gray-800 break-all">{{ $user->email }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 text-sm font-semibold opacity-75">Kelompok</p>
|
||||
@if($user->kelompok_asal)
|
||||
<span class="inline-block px-4 py-2 rounded-full text-sm font-bold text-white" style="background-color: {{ $user->kelompok_asal == 'IPA' ? '#0369A1' : '#B45309' }};">
|
||||
{{ $user->kelompok_asal }}
|
||||
</span>
|
||||
@else
|
||||
<p class="text-lg font-bold">-</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-700 text-sm font-semibold opacity-75">Terdaftar</p>
|
||||
<p class="text-lg font-semibold text-gray-800">{{ $user->created_at->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ========== INFORMASI PROFIL ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-blue-500">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-1">Informasi Profil</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-500 mb-6">Perbarui data diri dan foto profil Anda.</p>
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 mb-6 border-l-4 border-blue-500">
|
||||
<h2 class="text-xl font-bold text-maroon mb-1">📝 Edit Informasi Profil</h2>
|
||||
<p class="text-sm text-gray-500 mb-6">Perbarui data diri dan foto profil Anda.</p>
|
||||
|
||||
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('patch')
|
||||
|
||||
{{-- Foto Profil --}}
|
||||
{{-- Foto Profil Upload --}}
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-semibold text-maroon mb-2">Foto Profil</label>
|
||||
<div class="flex items-center gap-4">
|
||||
@if($user->foto)
|
||||
<img src="{{ asset($user->foto) }}" alt="Foto Profil" class="h-20 w-20 rounded-full object-cover border-2 border-maroon shadow" id="foto-preview">
|
||||
@else
|
||||
<div class="h-20 w-20 rounded-full bg-gray-200 flex items-center justify-center text-3xl border-2 border-maroon" id="foto-placeholder">
|
||||
👤
|
||||
</div>
|
||||
<img src="#" alt="Foto Profil" class="h-20 w-20 rounded-full object-cover border-2 border-maroon shadow hidden" id="foto-preview">
|
||||
@endif
|
||||
<label class="block text-sm font-semibold text-maroon mb-3">🖼️ Foto Profil Baru</label>
|
||||
<div class="flex items-center gap-6">
|
||||
<div>
|
||||
<input type="file" name="foto" id="foto" accept="image/*" class="text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-yellow-100 file:text-yellow-800 hover:file:bg-yellow-200" onchange="previewFoto(this)">
|
||||
<p class="text-xs text-gray-400 mt-1">Format: JPG, PNG, GIF. Maks 2MB.</p>
|
||||
<p class="text-xs text-gray-400 mt-2">Format: JPG, PNG, GIF. Maks 2MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
@error('foto')
|
||||
|
|
@ -95,60 +133,62 @@
|
|||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Nama --}}
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block text-sm font-semibold text-maroon mb-1">Nama Lengkap</label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
||||
@error('name')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{{-- Nama --}}
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-semibold text-maroon mb-2">Nama Lengkap</label>
|
||||
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
||||
@error('name')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-semibold text-maroon mb-2">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
||||
@error('email')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- NIS --}}
|
||||
<div>
|
||||
<label for="nis" class="block text-sm font-semibold text-maroon mb-2">NIS (Nomor Induk Siswa)</label>
|
||||
<input type="text" id="nis" name="nis" value="{{ old('nis', $user->nis) }}" maxlength="20"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" placeholder="Contoh: 123456">
|
||||
@error('nis')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Kelompok Asal --}}
|
||||
<div>
|
||||
<label for="kelompok_asal" class="block text-sm font-semibold text-maroon mb-2">Kelompok Asal</label>
|
||||
<select id="kelompok_asal" name="kelompok_asal"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0 bg-white">
|
||||
<option value="">-- Pilih Kelompok --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
@error('kelompok_asal')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="mb-4">
|
||||
<label for="email" class="block text-sm font-semibold text-maroon mb-1">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}" required
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0">
|
||||
@error('email')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- NIS --}}
|
||||
<div class="mb-4">
|
||||
<label for="nis" class="block text-sm font-semibold text-maroon mb-1">NIS (Nomor Induk Siswa)</label>
|
||||
<input type="text" id="nis" name="nis" value="{{ old('nis', $user->nis) }}" maxlength="20"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" placeholder="Contoh: 123456">
|
||||
@error('nis')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Kelompok Asal --}}
|
||||
<div class="mb-6">
|
||||
<label for="kelompok_asal" class="block text-sm font-semibold text-maroon mb-1">Kelompok Asal</label>
|
||||
<select id="kelompok_asal" name="kelompok_asal"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0 bg-white">
|
||||
<option value="">-- Pilih Kelompok --</option>
|
||||
<option value="IPA" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPA' ? 'selected' : '' }}>IPA</option>
|
||||
<option value="IPS" {{ old('kelompok_asal', $user->kelompok_asal) === 'IPS' ? 'selected' : '' }}>IPS</option>
|
||||
</select>
|
||||
@error('kelompok_asal')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-maroon font-bold py-2 px-6 rounded-lg text-sm">
|
||||
Simpan Perubahan
|
||||
<button type="submit" class="btn-maroon font-bold py-2 px-8 rounded-lg text-sm mt-6">
|
||||
💾 Simpan Perubahan
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ========== UBAH PASSWORD ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-green-500">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-maroon mb-1">Ubah Password</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-500 mb-6">Pastikan akun Anda menggunakan password yang kuat dan aman.</p>
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 mb-6 border-l-4 border-green-500">
|
||||
<h2 class="text-xl font-bold text-maroon mb-1">🔐 Ubah Password</h2>
|
||||
<p class="text-sm text-gray-500 mb-6">Pastikan akun Anda menggunakan password yang kuat dan aman.</p>
|
||||
|
||||
@if (session('status') === 'password-updated')
|
||||
<div class="bg-green-50 border border-green-300 text-green-800 rounded-lg p-4 mb-4 text-sm">
|
||||
|
|
@ -160,62 +200,64 @@ class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm fo
|
|||
@csrf
|
||||
@method('put')
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="current_password" class="block text-sm font-semibold text-maroon mb-1">Password Saat Ini</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="current_password" name="current_password"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('current_password', this)">👁️</button>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="current_password" class="block text-sm font-semibold text-maroon mb-2">Password Saat Ini</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="current_password" name="current_password"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('current_password', this)">👁️</button>
|
||||
</div>
|
||||
@error('current_password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-semibold text-maroon mb-2">Password Baru</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password" name="password"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password', this)">👁️</button>
|
||||
</div>
|
||||
@error('password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label for="password_confirmation" class="block text-sm font-semibold text-maroon mb-2">Konfirmasi Password Baru</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password_confirmation" name="password_confirmation"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password_confirmation', this)">👁️</button>
|
||||
</div>
|
||||
@error('password_confirmation', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@error('current_password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block text-sm font-semibold text-maroon mb-1">Password Baru</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password" name="password"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password', this)">👁️</button>
|
||||
</div>
|
||||
@error('password', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="password_confirmation" class="block text-sm font-semibold text-maroon mb-1">Konfirmasi Password Baru</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="password" id="password_confirmation" name="password_confirmation"
|
||||
class="input-focus w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-0" style="padding-right: 45px;">
|
||||
<button type="button" style="position: absolute; right: 12px; background: none; border: none; cursor: pointer; color: #5B7B89; font-size: 18px;" onclick="togglePasswordVisibility('password_confirmation', this)">👁️</button>
|
||||
</div>
|
||||
@error('password_confirmation', 'updatePassword')
|
||||
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg text-sm transition">
|
||||
Ubah Password
|
||||
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-8 rounded-lg text-sm transition mt-6">
|
||||
✓ Ubah Password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ========== HAPUS AKUN ========== --}}
|
||||
<div class="bg-white rounded-lg shadow-lg p-5 sm:p-8 mb-6 border-l-4 border-red-500">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-red-700 mb-1">Hapus Akun</h2>
|
||||
<p class="text-xs sm:text-sm text-gray-500 mb-4">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 sm:p-8 border-l-4 border-red-500">
|
||||
<h2 class="text-xl font-bold text-red-700 mb-1">⚠️ Hapus Akun</h2>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Setelah akun dihapus, semua data dan riwayat Anda akan dihapus secara permanen. Pastikan Anda sudah menyimpan data yang diperlukan.
|
||||
</p>
|
||||
|
||||
<button type="button" onclick="document.getElementById('delete-section').classList.toggle('hidden')" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg text-sm transition">
|
||||
Hapus Akun Saya
|
||||
🗑️ Hapus Akun Saya
|
||||
</button>
|
||||
|
||||
{{-- Konfirmasi Hapus --}}
|
||||
<div id="delete-section" class="hidden mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p class="text-sm text-red-800 mb-4 font-semibold">Apakah Anda yakin? Masukkan password untuk konfirmasi:</p>
|
||||
<p class="text-sm text-red-800 mb-4 font-semibold">⚠️ Apakah Anda yakin? Masukkan password untuk konfirmasi:</p>
|
||||
<form method="POST" action="{{ route('profile.destroy') }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
|
|
@ -253,11 +295,11 @@ function previewFoto(input) {
|
|||
if (input.files && input.files[0]) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var preview = document.getElementById('foto-preview');
|
||||
var placeholder = document.getElementById('foto-placeholder');
|
||||
preview.src = e.target.result;
|
||||
preview.classList.remove('hidden');
|
||||
if (placeholder) placeholder.classList.add('hidden');
|
||||
var previewHeader = document.getElementById('foto-preview-header');
|
||||
var placeholderHeader = document.getElementById('foto-placeholder-header');
|
||||
previewHeader.src = e.target.result;
|
||||
previewHeader.classList.remove('hidden');
|
||||
if (placeholderHeader) placeholderHeader.classList.add('hidden');
|
||||
}
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public function test_admin_can_add_jurusan_data()
|
|||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertDatabaseHas('polije_majors', ['nama_jurusan' => 'Informatika']);
|
||||
$this->assertDatabaseHas('jurusan_polije', ['nama_jurusan' => 'Informatika']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,7 +54,7 @@ public function test_bk_can_add_jurusan_data()
|
|||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertDatabaseHas('polije_majors', ['nama_jurusan' => 'Akuntansi']);
|
||||
$this->assertDatabaseHas('jurusan_polije', ['nama_jurusan' => 'Akuntansi']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue