[ 'keywords' => ['kotor', 'sampah', 'bersih', 'jorok', 'kumuh', 'toilet', 'wc', 'kamar mandi', 'limbah', 'bau'], 'color' => 'red', 'icon' => '๐Ÿงน', 'saran' => [ 'actions' => ['Tambah tempat sampah', 'Jadwal pembersihan rutin', 'Petugas kebersihan tambahan'], 'dampak' => 'Kepuasan meningkat signifikan', 'tip' => 'Tambah tempat sampah & jadwal pembersihan rutin.', ], ], 'Parkir' => [ 'keywords' => [ // FIX: hapus 'motor', 'mobil', 'kendaraan' โ€” terlalu ambigu, // sering muncul dalam konteks "harga tiket kendaraan" bukan keluhan parkir. // Hanya gunakan frasa yang secara spesifik merujuk pada area/masalah parkir. 'parkir', 'lahan parkir', 'tempat parkir', 'parkiran', 'bayar parkir', 'tarif parkir', 'biaya parkir', 'jukir', 'tukang parkir', 'penjaga parkir', 'petugas parkir', 'palak parkir', 'pungli parkir', ], 'color' => 'orange', 'icon' => '๐Ÿš—', 'saran' => [ 'actions' => ['Penataan area parkir', 'Tarif transparan', 'Petugas lebih ramah'], 'dampak' => 'Mengurangi keluhan', 'tip' => 'Sistem parkir lebih rapi dan tarif jelas.', ], ], 'Harga / Tiket' => [ 'keywords' => [ 'mahal', 'harga', 'tiket', 'bayar', 'biaya', 'tarif', 'murah', 'terjangkau', 'htm', 'retribusi', 'pungli', ], 'color' => 'yellow', 'icon' => '๐ŸŽŸ๏ธ', 'saran' => [ 'actions' => ['Evaluasi harga tiket masuk', 'Buat paket promo wisata', 'Transparansi retribusi'], 'dampak' => 'Daya tarik wisatawan meningkat', 'tip' => 'Penyesuaian HTM agar lebih terjangkau dan transparan.', ], ], 'Fasilitas' => [ 'keywords' => ['fasilitas', 'gazebo', 'warung', 'kantin', 'kamar ganti', 'musholla', 'wifi', 'bangku', 'kursi'], 'color' => 'green', 'icon' => '๐Ÿ—๏ธ', 'saran' => [ 'actions' => ['Perbaiki fasilitas rusak', 'Tambah fasilitas umum', 'Perawatan berkala'], 'dampak' => 'Kenyamanan pengunjung meningkat', 'tip' => 'Fasilitas lengkap & terawat meningkatkan kepuasan.', ], ], 'Keramaian' => [ 'keywords' => ['ramai', 'antri', 'penuh', 'sesak', 'macet', 'padat', 'berdesakan', 'keramaian', 'pengunjung banyak'], 'color' => 'blue', 'icon' => '๐Ÿ‘ฅ', 'saran' => [ 'actions' => ['Terapkan sistem reservasi', 'Batasi kapasitas pengunjung', 'Tambah jalur masuk'], 'dampak' => 'Kenyamanan kunjungan meningkat', 'tip' => 'Manajemen kapasitas pengunjung lebih baik.', ], ], ]; public function index(Request $request) { // --- 1. Dropdown destinasi --- $destinasiList = DB::table('hasil_analisis') ->select('wisata') ->distinct() ->pluck('wisata'); // --- 2. Resolve periode --- $periodeId = null; if ($request->filled('periode_id')) { $periodeId = (int) $request->periode_id; } elseif ($request->filled('periode_bulan')) { [$tahunParam, $bulanParam] = explode('-', $request->periode_bulan); $periodeRow = DB::table('periode_analisis') ->where('bulan', (int) $bulanParam) ->where('tahun', (int) $tahunParam) ->first(); $periodeId = $periodeRow?->id; } if (!$periodeId) { $periodeRow = DB::table('periode_analisis') ->where('bulan', now()->month) ->where('tahun', now()->year) ->first(); $periodeId = $periodeRow?->id; } // --- 3. Query builder --- $queryAll = HasilAnalisis::query(); $queryFilter = HasilAnalisis::query(); if ($periodeId) { $queryAll->where('periode_id', $periodeId); $queryFilter->where('periode_id', $periodeId); } if ($request->filled('destinasi')) { $queryFilter->where('wisata', $request->destinasi); } // --- 4. Hitung total & sentimen --- $totalUlasan = $queryFilter->count(); $totalNegatif = (clone $queryFilter)->where('sentimen', 'negatif')->count(); $totalPositif = (clone $queryFilter)->where('sentimen', 'positif')->count(); $totalNetral = (clone $queryFilter)->where('sentimen', 'netral')->count(); $persenNegatif = $totalUlasan > 0 ? round(($totalNegatif / $totalUlasan) * 100, 2) : 0; $persenPositif = $totalUlasan > 0 ? round(($totalPositif / $totalUlasan) * 100) : 0; $persenNetral = $totalUlasan > 0 ? round(($totalNetral / $totalUlasan) * 100) : 0; $tingkatKepuasan = $persenPositif; // --- 5. Statistik destinasi & periode --- $totalDestinasiAnalisis = (clone $queryAll)->distinct('wisata')->count('wisata'); $totalDestinasiBerkeluhan = (clone $queryAll)->where('sentimen', 'negatif') ->distinct('wisata')->count('wisata'); $periodeAktif = $periodeId ? DB::table('periode_analisis')->find($periodeId) : null; $labelKepuasan = match(true) { $tingkatKepuasan >= 80 => 'Baik', $tingkatKepuasan >= 60 => 'Sedang', default => 'Kurang', }; // ========================================================= // PERBAIKAN UTAMA: Hitung isu per ulasan (bukan per keyword) // Logika: 1 ulasan dihitung 1x untuk setiap kategori yang // keyword-nya ditemukan dalam teks ulasan tersebut. // Persentase = jumlah_ulasan_mengandung_kategori / total_ulasan_sentimen ร— 100 // ========================================================= $isuNegatif = $this->hitungIsuPerUlasan( (clone $queryFilter)->where('sentimen', 'negatif')->get(['ulasan_bersih', 'ulasan_asli']), $totalNegatif ); $isuNetral = $this->hitungIsuPerUlasan( (clone $queryFilter)->where('sentimen', 'netral')->get(['ulasan_bersih', 'ulasan_asli']), $totalNetral ); $isuPositif = $this->hitungIsuPerUlasan( (clone $queryFilter)->where('sentimen', 'positif')->get(['ulasan_bersih', 'ulasan_asli']), $totalPositif ); // isuUtama = sama dengan isuNegatif (untuk kompatibilitas variabel lama) $isuUtama = $isuNegatif; // --- 6. Isu dominan & prioritas rekomendasi --- if (empty($isuNegatif) || ($isuNegatif[0]['jumlah'] ?? 0) === 0) { $isuDominan = 'Tidak ada keluhan'; $isuDominanPersen = 0; $prioritas = []; $saranPerbaikan = []; } else { $isuDominan = $isuNegatif[0]['nama']; $isuDominanPersen = $isuNegatif[0]['persen']; // Prioritas: top 3 isu negatif berdasarkan jumlah ulasan $prioritas = []; $rank = 1; foreach (array_slice($isuNegatif, 0, 3) as $isu) { if ($isu['jumlah'] === 0) break; $nama = $isu['nama']; $prioritas[] = [ 'rank' => $rank++, 'nama' => $nama, 'icon' => $this->issueRules[$nama]['icon'], 'actions' => $this->issueRules[$nama]['saran']['actions'], 'dampak' => $this->issueRules[$nama]['saran']['dampak'], 'color' => $this->issueRules[$nama]['color'], 'jumlah' => $isu['jumlah'], 'persen' => $isu['persen'], ]; } $saranPerbaikan = []; foreach (array_slice($isuNegatif, 0, 3) as $isu) { if ($isu['jumlah'] === 0) break; $nama = $isu['nama']; $saranPerbaikan[] = [ 'nama' => 'Perbaikan ' . $nama, 'tip' => $this->issueRules[$nama]['saran']['tip'], 'icon' => $this->issueRules[$nama]['icon'], ]; } } // --- 7. Kata kunci dominan dari ulasan negatif --- $ulasanNegatifTeks = (clone $queryFilter) ->where('sentimen', 'negatif') ->get(['ulasan_bersih', 'ulasan_asli']) ->map(fn($r) => $r->ulasan_bersih ?? $r->ulasan_asli ?? '') ->filter() ->toArray(); $textNegatif = strtolower(implode(' ', $ulasanNegatifTeks)); $words = array_filter(preg_split('/\s+/', $textNegatif)); $stopwords = [ 'yang', 'dan', 'di', 'ke', 'dari', 'untuk', 'dengan', 'ini', 'itu', 'tidak', 'ada', 'juga', 'sangat', 'lebih', 'sudah', 'bisa', 'tapi', 'karena', 'pada', 'akan', 'atau', 'saya', 'kami', 'mereka', 'nya', 'ga', 'gak', 'udah', 'pas', 'kita', 'sih', 'deh', 'aja', 'ya', 'yg', 'tp', 'bgt', ]; $freq = array_count_values($words); foreach ($stopwords as $sw) { unset($freq[$sw]); } foreach (array_keys($freq) as $w) { if (mb_strlen($w) < 3) unset($freq[$w]); } arsort($freq); $kataDominan = array_slice($freq, 0, 10, true); $destinasiAktif = $request->input('destinasi', 'Semua Destinasi'); return view('rekomendasi.index', compact( 'destinasiList', 'destinasiAktif', 'periodeId', 'totalUlasan', 'totalNegatif', 'totalPositif', 'totalNetral', 'persenNegatif', 'persenPositif', 'persenNetral', 'tingkatKepuasan', 'labelKepuasan', 'totalDestinasiAnalisis', 'totalDestinasiBerkeluhan', 'periodeAktif', 'isuUtama', 'isuNegatif', 'isuNetral', 'isuPositif', 'isuDominan', 'isuDominanPersen', 'kataDominan', 'saranPerbaikan', 'prioritas', )); } /** * Hitung isu per ulasan โ€” BENAR. * * Setiap ulasan hanya dihitung SATU KALI per kategori meski * keyword-nya muncul berkali-kali dalam satu ulasan yang sama. * Persentase = (jumlah ulasan mengandung kategori / total ulasan) ร— 100 * * @param \Illuminate\Support\Collection $ulasanCollection hasil ->get(['ulasan_bersih','ulasan_asli']) * @param int $totalUlasan total ulasan pada sentimen yang sama * @return array top-5 isu, sudah diurutkan dari tertinggi */ private function hitungIsuPerUlasan($ulasanCollection, int $totalUlasan): array { // Siapkan teks tiap ulasan (fallback ke ulasan_asli jika ulasan_bersih kosong) $teksUlasan = $ulasanCollection ->map(fn($r) => strtolower($r->ulasan_bersih ?? $r->ulasan_asli ?? '')) ->filter() ->values(); // Hitung berapa ulasan yang menyebut tiap kategori $jumlahPerKategori = []; foreach ($this->issueRules as $kategori => $rule) { $jumlah = 0; foreach ($teksUlasan as $teks) { foreach ($rule['keywords'] as $kw) { if (str_contains($teks, $kw)) { $jumlah++; break; // 1 ulasan dihitung 1x per kategori } } } $jumlahPerKategori[$kategori] = $jumlah; } // Urutkan dari terbanyak arsort($jumlahPerKategori); // Bangun array hasil top-5 $hasil = []; foreach (array_slice($jumlahPerKategori, 0, 5, true) as $nama => $jumlah) { $persen = $totalUlasan > 0 ? round(($jumlah / $totalUlasan) * 100) : 0; $hasil[] = [ 'nama' => $nama, 'jumlah' => $jumlah, 'persen' => $persen, 'color' => $this->issueRules[$nama]['color'], 'icon' => $this->issueRules[$nama]['icon'], ]; } return $hasil; } }