diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..61af4ef --- /dev/null +++ b/.phpunit.result.cache @@ -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}} \ No newline at end of file diff --git a/PANDUAN_IMPORT_SISWA.txt b/PANDUAN_IMPORT_SISWA.txt new file mode 100644 index 0000000..aa1617c --- /dev/null +++ b/PANDUAN_IMPORT_SISWA.txt @@ -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! diff --git a/TEMPLATE_ALUMNI_IMPORT.csv b/TEMPLATE_ALUMNI_IMPORT.csv new file mode 100644 index 0000000..fba1981 --- /dev/null +++ b/TEMPLATE_ALUMNI_IMPORT.csv @@ -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 diff --git a/TEMPLATE_ALUMNI_MINIMAL.csv b/TEMPLATE_ALUMNI_MINIMAL.csv new file mode 100644 index 0000000..96eac46 --- /dev/null +++ b/TEMPLATE_ALUMNI_MINIMAL.csv @@ -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, diff --git a/TEMPLATE_IMPORT_ALUMNI_BIMA_AMBULU.md b/TEMPLATE_IMPORT_ALUMNI_BIMA_AMBULU.md new file mode 100644 index 0000000..31641e7 --- /dev/null +++ b/TEMPLATE_IMPORT_ALUMNI_BIMA_AMBULU.md @@ -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. diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 8c868c4..27363ed 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -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}%"); }); diff --git a/app/Http/Controllers/BKController.php b/app/Http/Controllers/BKController.php index 79d9f68..8dcf062 100644 --- a/app/Http/Controllers/BKController.php +++ b/app/Http/Controllers/BKController.php @@ -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', diff --git a/app/Http/Controllers/ChatbotController.php b/app/Http/Controllers/ChatbotController.php index d5a33f9..3a434ef 100644 --- a/app/Http/Controllers/ChatbotController.php +++ b/app/Http/Controllers/ChatbotController.php @@ -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, ]; } diff --git a/app/Models/ChatHistory.php b/app/Models/ChatHistory.php index 0dcf8a5..6aae023 100644 --- a/app/Models/ChatHistory.php +++ b/app/Models/ChatHistory.php @@ -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'); } } diff --git a/app/Models/PolijeMajor.php b/app/Models/PolijeMajor.php index 21d3ae6..160b958 100644 --- a/app/Models/PolijeMajor.php +++ b/app/Models/PolijeMajor.php @@ -9,6 +9,8 @@ class PolijeMajor extends Model { use HasFactory; + protected $table = 'jurusan_polije'; + protected $fillable = [ 'nama_jurusan', 'deskripsi', diff --git a/app/Models/Recommendation.php b/app/Models/Recommendation.php index cf9fc6a..e6f0c07 100644 --- a/app/Models/Recommendation.php +++ b/app/Models/Recommendation.php @@ -9,6 +9,8 @@ class Recommendation extends Model { use HasFactory; + protected $table = 'rekomendasi'; + protected $fillable = [ 'user_id', 'mtk', diff --git a/app/Services/GeminiService.php b/app/Services/GeminiService.php index 49603af..82c5972 100644 --- a/app/Services/GeminiService.php +++ b/app/Services/GeminiService.php @@ -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.', ]; } } diff --git a/database/migrations/2026_04_22_000000_rename_domain_tables_to_indonesian.php b/database/migrations/2026_04_22_000000_rename_domain_tables_to_indonesian.php new file mode 100644 index 0000000..fe21e0b --- /dev/null +++ b/database/migrations/2026_04_22_000000_rename_domain_tables_to_indonesian.php @@ -0,0 +1,86 @@ +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'); + } + } +}; diff --git a/database/seeders/BimaAmbulustudentSeeder.php b/database/seeders/BimaAmbulustudentSeeder.php new file mode 100644 index 0000000..8c88307 --- /dev/null +++ b/database/seeders/BimaAmbulustudentSeeder.php @@ -0,0 +1,223 @@ + '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; + } +} diff --git a/database/seeders/RegenerateRecommendationsSeeder.php b/database/seeders/RegenerateRecommendationsSeeder.php new file mode 100644 index 0000000..d668961 --- /dev/null +++ b/database/seeders/RegenerateRecommendationsSeeder.php @@ -0,0 +1,84 @@ +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!"); + } +} diff --git a/database/seeders/StudentRecommendationSeeder.php b/database/seeders/StudentRecommendationSeeder.php new file mode 100644 index 0000000..b5506d1 --- /dev/null +++ b/database/seeders/StudentRecommendationSeeder.php @@ -0,0 +1,286 @@ + ['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'], + ]); + } + } +} diff --git a/database/seeders/StudentWithAccurateRecommendationSeeder.php b/database/seeders/StudentWithAccurateRecommendationSeeder.php new file mode 100644 index 0000000..42186bf --- /dev/null +++ b/database/seeders/StudentWithAccurateRecommendationSeeder.php @@ -0,0 +1,248 @@ + '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; + } +} diff --git a/database/seeders/UpdateMajorsAccurateBobotSeeder.php b/database/seeders/UpdateMajorsAccurateBobotSeeder.php new file mode 100644 index 0000000..bf7dac1 --- /dev/null +++ b/database/seeders/UpdateMajorsAccurateBobotSeeder.php @@ -0,0 +1,162 @@ + '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!'); + } +} diff --git a/public/python_backend/app.py b/public/python_backend/app.py index b527143..dc11643 100644 --- a/public/python_backend/app.py +++ b/public/python_backend/app.py @@ -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__": diff --git a/public/python_backend/backend.log b/public/python_backend/backend.log new file mode 100644 index 0000000..ee48a5e --- /dev/null +++ b/public/python_backend/backend.log @@ -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 diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 7962ba4..0f2254d 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -23,40 +23,41 @@ - +
Belum ada data rekomendasi
- @endif +SPK Jurusan
-Admin Panel
+Admin Panel
{{ Auth::user()->name }}
-{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}
+{{ Auth::user()->name }}
+{{ Auth::user()->role === 'admin' ? 'Administrator' : (Auth::user()->role === 'bk' ? 'Guru BK' : ucfirst(Auth::user()->role)) }}
Nama
-{{ $student->name }}
-{{ $student->email }}
-NIS
-{{ $student->nis ?? '-' }}
-Kelompok Asal
- @if($student->kelompok_asal) -- - {{ $student->kelompok_asal }} - -
- @else --
- @endif -Foto Profil
+ +-
+Terdaftar
-{{ $student->created_at->format('d M Y H:i') }}
-{{ $rec->created_at->format('d M Y H:i') }}
- Rekomendasi #{{ $loop->index + 1 }} -Top 3 Rekomendasi:
-NIS
+{{ $student->nis ?? '-' }}
+Kelompok
+ @if($student->kelompok_asal) + + {{ $student->kelompok_asal }} + + @else +-
@endif - -Minat: {{ $rec->minat ?? '-' }} | Cita-cita: {{ $rec->cita_cita ?? '-' }}
-{{ $student->email }}
+Terdaftar
+{{ $student->created_at->format('d M Y') }}
+Siswa belum melakukan rekomendasi
- @endif +{{ $rec->created_at->format('d M Y H:i') }}
+ Rekomendasi #{{ $loop->index + 1 }} +📝 Minat: {{ $rec->minat ?? '-' }} | 🎓 Cita-cita: {{ $rec->cita_cita ?? '-' }}
+Siswa belum melakukan rekomendasi
+{{ $chat->created_at->format('d M Y H:i') }}
-👤 Pertanyaan Siswa:
-{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}
-🤖 Jawaban AI:
-{{ \Illuminate\Support\Str::limit($chat->response, 150) }}
-{{ $chat->created_at->format('d M Y') }}
+Q: {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}
+A: {{ \Illuminate\Support\Str::limit($chat->response, 80) }}
+Belum ada chat
+ @endifSiswa belum melakukan chat dengan AI
- @endif +Belum ada data rekomendasi
- @endif +Nama
-{{ $student->name }}
-{{ $student->email }}
-NIS
-{{ $student->nis ?? '-' }}
-Kelompok Asal
- @if($student->kelompok_asal) -- - {{ $student->kelompok_asal }} - -
- @else --
- @endif -Foto Profil
+ +-
+Terdaftar
-{{ $student->created_at->format('d M Y H:i') }}
-{{ $rec->created_at->format('d M Y H:i') }}
- Rekomendasi #{{ $loop->index + 1 }} -Top 3 Rekomendasi:
-NIS
+{{ $student->nis ?? '-' }}
+Kelompok
+ @if($student->kelompok_asal) + + {{ $student->kelompok_asal }} + + @else +-
@endif - -Minat: {{ $rec->minat ?? '-' }} | Cita-cita: {{ $rec->cita_cita ?? '-' }}
-{{ $student->email }}
+Terdaftar
+{{ $student->created_at->format('d M Y') }}
+Siswa belum melakukan rekomendasi
- @endif +{{ $rec->created_at->format('d M Y H:i') }}
+ Rekomendasi #{{ $loop->index + 1 }} +📝 Minat: {{ $rec->minat ?? '-' }} | 🎓 Cita-cita: {{ $rec->cita_cita ?? '-' }}
+Siswa belum melakukan rekomendasi
+{{ $chat->created_at->format('d M Y H:i') }}
-👤 Pertanyaan Siswa:
-{{ \Illuminate\Support\Str::limit($chat->prompt, 150) }}
-🤖 Jawaban AI:
-{{ \Illuminate\Support\Str::limit($chat->response, 150) }}
-{{ $chat->created_at->format('d M Y') }}
+Q: {{ \Illuminate\Support\Str::limit($chat->prompt, 80) }}
+A: {{ \Illuminate\Support\Str::limit($chat->response, 80) }}
+Belum ada chat
+ @endifSiswa belum melakukan chat dengan AI
- @endif +Semua Analisis Anda
-{{ $rec->created_at->diffForHumans() }}
+{{ $rec->created_at->diffForHumans() }}
+Minat
-{{ $rec->minat ?? '-' }}
-Pref. Belajar
-{{ $rec->preferensi_studi ?? '-' }}
-Cita-Cita
-{{ Str::limit($rec->cita_cita ?? '-', 15) }}
-Prestasi
-{{ Str::limit($rec->prestasi ?? '-', 15) }}
+ +💭 Minat
+{{ $rec->minat ?? '-' }}
+🎓 Preferensi Studi
+{{ $rec->preferensi_studi ?? '-' }}
+🎯 Cita-Cita
+{{ $rec->cita_cita ?? '-' }}
+🏆 Prestasi
+{{ $rec->prestasi ?? '-' }}
+{{ $subject['icon'] }} {{ $subject['name'] }}
+{{ $rec->{$subject['key']} }}
+Rekomendasi {{ $index + 1 }}
+{{ $hasil['jurusan'] ?? '-' }}
+Anda belum melakukan analisis rekomendasi. Mulai sekarang!
- - Mulai Analisis +Anda belum melakukan analisis rekomendasi. Mulai sekarang untuk melihat history!
+ + 🚀 Mulai Analisis