nambah aja
This commit is contained in:
parent
227aad6557
commit
8e78dce219
|
|
@ -4,30 +4,29 @@
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\HasilAnalisis;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AnalisisController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// Dropdown list destinasi unik
|
||||
// Ambil data evaluasi model
|
||||
$evaluasi = DB::table('evaluasi_model')->latest()->first();
|
||||
|
||||
// Dropdown wisata
|
||||
$wisataList = HasilAnalisis::select('wisata')
|
||||
->distinct()
|
||||
->pluck('wisata');
|
||||
|
||||
// Query utama tabel
|
||||
// Query hasil analisis dengan filter
|
||||
$query = HasilAnalisis::query();
|
||||
|
||||
// Filter kalau user pilih destinasi tertentu
|
||||
if ($request->filled('wisata')) {
|
||||
$query->where('wisata', $request->wisata);
|
||||
}
|
||||
|
||||
// Data hasil analisis (pakai paginate biar rapi)
|
||||
$hasil = $query->latest()->paginate(5);
|
||||
$hasil = $query->latest()->paginate(10);
|
||||
|
||||
return view('analisis.index', compact(
|
||||
'hasil',
|
||||
'wisataList'
|
||||
));
|
||||
return view('analisis.index', compact('hasil', 'wisataList', 'evaluasi'));
|
||||
}
|
||||
}
|
||||
|
|
@ -7,138 +7,302 @@
|
|||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$wisata = $request->wisata;
|
||||
private array $issueRules = [
|
||||
'Kebersihan' => [
|
||||
'keywords' => ['kotor', 'sampah', 'jorok', 'kumuh', 'toilet', 'wc', 'kamar mandi', 'limbah', 'bau', 'bersih'],
|
||||
'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' => ['parkir', 'lahan parkir', 'tempat parkir', 'parkiran', 'motor', 'mobil'],
|
||||
'color' => 'orange',
|
||||
'icon' => '🚗',
|
||||
'saran' => [
|
||||
'actions' => ['Penataan area parkir', 'Tarif transparan', 'Petugas lebih ramah'],
|
||||
'dampak' => 'Mengurangi keluhan',
|
||||
'tip' => 'Sistem parkir lebih rapi dan transparan.',
|
||||
],
|
||||
],
|
||||
'Harga / Tiket' => [
|
||||
'keywords' => ['mahal', 'harga', 'tiket', 'bayar', 'biaya', 'tarif', 'terjangkau'],
|
||||
'color' => 'yellow',
|
||||
'icon' => '🎟️',
|
||||
'saran' => [
|
||||
'actions' => ['Evaluasi harga tiket', 'Promo wisata', 'Diskon tertentu'],
|
||||
'dampak' => 'Daya tarik meningkat',
|
||||
'tip' => 'Penyesuaian harga tiket agar lebih terjangkau.',
|
||||
],
|
||||
],
|
||||
'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', 'macet', 'antri', 'sesak', 'penuh', 'padat', 'berdesakan'],
|
||||
'color' => 'blue',
|
||||
'icon' => '👥',
|
||||
'saran' => [
|
||||
'actions' => ['Atur kapasitas pengunjung', 'Sistem antrean', 'Jam kunjungan fleksibel'],
|
||||
'dampak' => 'Pengalaman pengunjung lebih nyaman',
|
||||
'tip' => 'Manajemen kapasitas pengunjung lebih baik.',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$wisata = $request->wisata;
|
||||
$destinasi = $request->destinasi;
|
||||
|
||||
// ================================================================
|
||||
// AMBIL SEMUA PERIODE & PERIODE AKTIF
|
||||
// ================================================================
|
||||
$periodeList = DB::table('periode_analisis')->orderBy('id', 'desc')->get();
|
||||
$periodeAktif = $request->periode_id
|
||||
? DB::table('periode_analisis')->find($request->periode_id)
|
||||
: DB::table('periode_analisis')->orderBy('id', 'desc')->first();
|
||||
$periodeId = $periodeAktif->id ?? null;
|
||||
|
||||
$evaluasi = DB::table('evaluasi_model')
|
||||
->latest()
|
||||
->first();
|
||||
// =============================
|
||||
// TOTAL ULASAN (DARI TABEL ULASAN)
|
||||
// =============================
|
||||
$totalUlasan = DB::table('ulasan')
|
||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
||||
$q->where('wisata', $wisata);
|
||||
})
|
||||
->count();
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->latest()->first();
|
||||
|
||||
// =============================
|
||||
// CEK ADA ANALISIS ATAU BELUM
|
||||
// =============================
|
||||
$hasAnalisis = DB::table('hasil_analisis')->count() > 0;
|
||||
// ================================================================
|
||||
// TOTAL ULASAN
|
||||
// ================================================================
|
||||
$totalUlasan = DB::table('ulasan')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->count();
|
||||
|
||||
// =============================
|
||||
// HITUNG SENTIMEN (DARI HASIL ANALISIS)
|
||||
// =============================
|
||||
$positif = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Positif')
|
||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
||||
$q->where('wisata', $wisata);
|
||||
})
|
||||
->count();
|
||||
// ================================================================
|
||||
// CEK ADA ANALISIS ATAU BELUM
|
||||
// ================================================================
|
||||
$hasAnalisis = DB::table('hasil_analisis')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->count() > 0;
|
||||
|
||||
$negatif = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Negatif')
|
||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
||||
$q->where('wisata', $wisata);
|
||||
})
|
||||
->count();
|
||||
// ================================================================
|
||||
// HITUNG SENTIMEN
|
||||
// ================================================================
|
||||
$positif = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Positif')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->count();
|
||||
|
||||
$netral = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Netral')
|
||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
||||
$q->where('wisata', $wisata);
|
||||
})
|
||||
->count();
|
||||
$negatif = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Negatif')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->count();
|
||||
|
||||
// =============================
|
||||
// PERSENTASE SENTIMEN
|
||||
// =============================
|
||||
$totalSentimen = $positif + $negatif + $netral;
|
||||
$netral = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Netral')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->count();
|
||||
|
||||
$stats = [
|
||||
'total' => $totalUlasan,
|
||||
'positif_persen' => $totalSentimen > 0 ? round(($positif / $totalSentimen) * 100) : 0,
|
||||
'negatif_persen' => $totalSentimen > 0 ? round(($negatif / $totalSentimen) * 100) : 0,
|
||||
'netral_persen' => $totalSentimen > 0 ? round(($netral / $totalSentimen) * 100) : 0,
|
||||
];
|
||||
$totalSentimen = $positif + $negatif + $netral;
|
||||
|
||||
// =============================
|
||||
// GRAFIK PIE (Distribusi Sentimen)
|
||||
// =============================
|
||||
$chartSentimen = $hasAnalisis
|
||||
? DB::table('hasil_analisis')
|
||||
->select('sentimen', DB::raw('count(*) as total'))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
||||
$q->where('wisata', $wisata);
|
||||
})
|
||||
->groupBy('sentimen')
|
||||
->orderByRaw("FIELD(sentimen, 'Positif', 'Negatif', 'Netral')") // 🔥 INI KUNCINYA
|
||||
->get()
|
||||
: collect();
|
||||
$stats = [
|
||||
'total' => $totalUlasan,
|
||||
'positif_persen' => $totalSentimen > 0 ? round(($positif / $totalSentimen) * 100) : 0,
|
||||
'negatif_persen' => $totalSentimen > 0 ? round(($negatif / $totalSentimen) * 100) : 0,
|
||||
'netral_persen' => $totalSentimen > 0 ? round(($netral / $totalSentimen) * 100) : 0,
|
||||
];
|
||||
|
||||
// =============================
|
||||
// GRAFIK BAR (Sentimen per Destinasi)
|
||||
// =============================
|
||||
$chartDestinasi = $hasAnalisis
|
||||
? DB::table('hasil_analisis')
|
||||
->select(
|
||||
'wisata',
|
||||
DB::raw("SUM(CASE WHEN sentimen = 'Positif' THEN 1 ELSE 0 END) as positif"),
|
||||
DB::raw("SUM(CASE WHEN sentimen = 'Negatif' THEN 1 ELSE 0 END) as negatif"),
|
||||
DB::raw("SUM(CASE WHEN sentimen = 'Netral' THEN 1 ELSE 0 END) as netral")
|
||||
)
|
||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
||||
$q->where('wisata', $wisata);
|
||||
})
|
||||
->groupBy('wisata')
|
||||
|
||||
->get()
|
||||
: collect();
|
||||
// ================================================================
|
||||
// CHART DATA
|
||||
// ================================================================
|
||||
$chartSentimen = $hasAnalisis
|
||||
? DB::table('hasil_analisis')
|
||||
->select('sentimen', DB::raw('count(*) as total'))
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->groupBy('sentimen')
|
||||
->orderByRaw("FIELD(sentimen, 'Positif', 'Negatif', 'Netral')")
|
||||
->get()
|
||||
: collect();
|
||||
|
||||
// =============================
|
||||
// TOTAL DATA PER DESTINASI (DARI ULASAN)
|
||||
// =============================
|
||||
$totalPerWisata = DB::table('ulasan')
|
||||
->select('wisata', DB::raw('count(*) as total'))
|
||||
$chartDestinasi = $hasAnalisis
|
||||
? DB::table('hasil_analisis')
|
||||
->select(
|
||||
'wisata',
|
||||
DB::raw("SUM(CASE WHEN sentimen = 'Positif' THEN 1 ELSE 0 END) as positif"),
|
||||
DB::raw("SUM(CASE WHEN sentimen = 'Negatif' THEN 1 ELSE 0 END) as negatif"),
|
||||
DB::raw("SUM(CASE WHEN sentimen = 'Netral' THEN 1 ELSE 0 END) as netral")
|
||||
)
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->groupBy('wisata')
|
||||
->get();
|
||||
->get()
|
||||
: collect();
|
||||
|
||||
// =============================
|
||||
// WORD CLOUD (DARI PREPROCESSING)
|
||||
// =============================
|
||||
$texts = DB::table('preprocessing_data')->pluck('stemming')->toArray();
|
||||
$allText = count($texts) > 0 ? implode(' ', $texts) : '';
|
||||
// ================================================================
|
||||
// TOTAL PER DESTINASI + WORD CLOUD + LAST UPDATE
|
||||
// ================================================================
|
||||
$totalPerWisata = DB::table('ulasan')
|
||||
->select('wisata', DB::raw('count(*) as total'))
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->groupBy('wisata')
|
||||
->get();
|
||||
|
||||
// =============================
|
||||
// LAST UPDATE
|
||||
// =============================
|
||||
$lastUpdate = DB::table('hasil_analisis')
|
||||
->latest('created_at')
|
||||
->value('created_at');
|
||||
$texts = DB::table('hasil_analisis')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->pluck('ulasan_bersih')
|
||||
->toArray();
|
||||
$allText = count($texts) > 0 ? implode(' ', $texts) : '';
|
||||
|
||||
$destinasiList = DB::table('ulasan')
|
||||
->select('wisata')
|
||||
->distinct()
|
||||
->pluck('wisata');
|
||||
$lastUpdate = DB::table('hasil_analisis')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->latest('created_at')->value('created_at');
|
||||
|
||||
$hasil = DB::table('hasil_analisis')
|
||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
||||
$q->where('wisata', $wisata);
|
||||
})
|
||||
->paginate(10);
|
||||
$destinasiList = DB::table('ulasan')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->select('wisata')->distinct()->pluck('wisata');
|
||||
|
||||
return view('dashboard', compact(
|
||||
'stats',
|
||||
'chartSentimen',
|
||||
'chartDestinasi',
|
||||
'totalPerWisata',
|
||||
'allText',
|
||||
'lastUpdate',
|
||||
'hasAnalisis',
|
||||
'destinasiList', // WAJIB
|
||||
'hasil',
|
||||
'evaluasi' // WAJIB
|
||||
));
|
||||
$hasil = DB::table('hasil_analisis')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||
->paginate(10);
|
||||
|
||||
// ================================================================
|
||||
// REKOMENDASI
|
||||
// ================================================================
|
||||
$ulasanNegatif = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Negatif')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($destinasi && $destinasi != 'Semua Destinasi', fn($q) => $q->where('wisata', $destinasi))
|
||||
->pluck('ulasan_bersih')
|
||||
->toArray();
|
||||
|
||||
$textRek = strtolower(implode(' ', $ulasanNegatif));
|
||||
$wordsRek = array_filter(preg_split('/\s+/', $textRek));
|
||||
|
||||
$issueSkor = [];
|
||||
foreach ($this->issueRules as $kategori => $rule) {
|
||||
$skor = 0;
|
||||
foreach ($rule['keywords'] as $kw) {
|
||||
$skor += substr_count($textRek, $kw);
|
||||
}
|
||||
$issueSkor[$kategori] = $skor;
|
||||
}
|
||||
arsort($issueSkor);
|
||||
$totalSkorIsu = array_sum($issueSkor) ?: 1;
|
||||
|
||||
$isuUtama = [];
|
||||
foreach (array_slice($issueSkor, 0, 5, true) as $nama => $skor) {
|
||||
$isuUtama[] = [
|
||||
'nama' => $nama,
|
||||
'skor' => $skor,
|
||||
'persen' => round(($skor / $totalSkorIsu) * 100),
|
||||
'color' => $this->issueRules[$nama]['color'],
|
||||
'icon' => $this->issueRules[$nama]['icon'],
|
||||
];
|
||||
}
|
||||
|
||||
$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','jadi','biar','kalau',
|
||||
];
|
||||
$freq = array_count_values($wordsRek);
|
||||
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);
|
||||
|
||||
$saranPerbaikan = [];
|
||||
foreach (array_slice($issueSkor, 0, 3, true) as $nama => $skor) {
|
||||
$saranPerbaikan[] = [
|
||||
'nama' => 'Perbaikan ' . $nama,
|
||||
'tip' => $this->issueRules[$nama]['saran']['tip'],
|
||||
'icon' => $this->issueRules[$nama]['icon'],
|
||||
];
|
||||
}
|
||||
|
||||
$prioritas = [];
|
||||
$rank = 1;
|
||||
foreach (array_slice($issueSkor, 0, 3, true) as $nama => $skor) {
|
||||
$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'],
|
||||
];
|
||||
}
|
||||
|
||||
$positifRek = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Positif')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($destinasi && $destinasi != 'Semua Destinasi', fn($q) => $q->where('wisata', $destinasi))
|
||||
->count();
|
||||
|
||||
$negatifRek = DB::table('hasil_analisis')
|
||||
->where('sentimen', 'Negatif')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($destinasi && $destinasi != 'Semua Destinasi', fn($q) => $q->where('wisata', $destinasi))
|
||||
->count();
|
||||
|
||||
$totalSentimenRek = DB::table('hasil_analisis')
|
||||
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||
->when($destinasi && $destinasi != 'Semua Destinasi', fn($q) => $q->where('wisata', $destinasi))
|
||||
->count() ?: 1;
|
||||
|
||||
$totalNegatif = $negatifRek;
|
||||
$persenNegatif = round(($negatifRek / $totalSentimenRek) * 100, 2);
|
||||
$tingkatKepuasan = round(($positifRek / $totalSentimenRek) * 100);
|
||||
$labelKepuasan = match(true) {
|
||||
$tingkatKepuasan >= 80 => 'Baik',
|
||||
$tingkatKepuasan >= 60 => 'Sedang',
|
||||
default => 'Kurang',
|
||||
};
|
||||
$isuDominan = $isuUtama[0]['nama'] ?? '-';
|
||||
$isuDominanPersen = $isuUtama[0]['persen'] ?? 0;
|
||||
|
||||
// ================================================================
|
||||
// RETURN VIEW
|
||||
// ================================================================
|
||||
return view('dashboard', compact(
|
||||
'stats',
|
||||
'chartSentimen',
|
||||
'chartDestinasi',
|
||||
'totalPerWisata',
|
||||
'allText',
|
||||
'lastUpdate',
|
||||
'hasAnalisis',
|
||||
'destinasiList',
|
||||
'hasil',
|
||||
'evaluasi',
|
||||
'totalNegatif',
|
||||
'persenNegatif',
|
||||
'tingkatKepuasan',
|
||||
'labelKepuasan',
|
||||
'isuUtama',
|
||||
'isuDominan',
|
||||
'isuDominanPersen',
|
||||
'kataDominan',
|
||||
'saranPerbaikan',
|
||||
'prioritas',
|
||||
'periodeList', // ← tambahan
|
||||
'periodeAktif', // ← tambahan
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,65 +8,208 @@
|
|||
|
||||
class RekomendasiController extends Controller
|
||||
{
|
||||
/**
|
||||
* Rule-based: mapping kategori isu ke keyword terkait
|
||||
*/
|
||||
private array $issueRules = [
|
||||
'Kebersihan' => [
|
||||
'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' => ['parkir', 'lahan parkir', 'tempat parkir', 'parkiran', 'motor', 'mobil'],
|
||||
'color' => 'orange',
|
||||
'icon' => '🚗',
|
||||
'saran' => [
|
||||
'actions' => ['Penataan area parkir', 'Tarif transparan', 'Petugas lebih ramah'],
|
||||
'dampak' => 'Mengurangi keluhan',
|
||||
'tip' => 'Sistem parkir lebih rapi dan transparan.',
|
||||
],
|
||||
],
|
||||
'Harga / Tiket' => [
|
||||
'keywords' => ['mahal', 'harga', 'tiket', 'bayar', 'biaya', 'tarif', 'murah', 'terjangkau'],
|
||||
'color' => 'yellow',
|
||||
'icon' => '🎟️',
|
||||
'saran' => [
|
||||
'actions' => ['Evaluasi harga tiket', 'Promo wisata', 'Diskon tertentu'],
|
||||
'dampak' => 'Daya tarik meningkat',
|
||||
'tip' => 'Penyesuaian harga tiket agar lebih terjangkau.',
|
||||
],
|
||||
],
|
||||
'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', 'macet', 'antri', 'sesak', 'penuh', 'padat', 'berdesakan'],
|
||||
'color' => 'blue',
|
||||
'icon' => '👥',
|
||||
'saran' => [
|
||||
'actions' => ['Atur kapasitas pengunjung', 'Sistem antrean', 'Jam kunjungan fleksibel'],
|
||||
'dampak' => 'Pengalaman pengunjung lebih nyaman',
|
||||
'tip' => 'Manajemen kapasitas pengunjung lebih baik.',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
// dropdown destinasi unik
|
||||
$destinasiList = HasilAnalisis::select('wisata')
|
||||
// --- 1. Dropdown destinasi ---
|
||||
$destinasiList = DB::table('hasil_analisis')
|
||||
->select('wisata')
|
||||
->distinct()
|
||||
->pluck('wisata');
|
||||
|
||||
// query utama
|
||||
$query = HasilAnalisis::query();
|
||||
// --- 2. Query dengan filter destinasi ---
|
||||
$queryAll = HasilAnalisis::query();
|
||||
$queryFilter = HasilAnalisis::query();
|
||||
|
||||
// filter destinasi jika dipilih
|
||||
if ($request->filled('destinasi')) {
|
||||
$query->where('wisata', $request->destinasi);
|
||||
$queryFilter->where('wisata', $request->destinasi);
|
||||
}
|
||||
|
||||
// ambil ulasan negatif
|
||||
$negatif = $query->where('sentimen', 'negatif')
|
||||
->pluck('ulasan_terolah')
|
||||
// --- 3. 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;
|
||||
$tingkatKepuasan = $totalUlasan > 0 ? round(($totalPositif / $totalUlasan) * 100) : 0;
|
||||
|
||||
// Label kepuasan
|
||||
$labelKepuasan = match(true) {
|
||||
$tingkatKepuasan >= 80 => 'Baik',
|
||||
$tingkatKepuasan >= 60 => 'Sedang',
|
||||
default => 'Kurang',
|
||||
};
|
||||
|
||||
// --- 4. Ambil ulasan negatif & bersihkan teks ---
|
||||
$ulasanNegatif = (clone $queryFilter)
|
||||
->where('sentimen', 'negatif')
|
||||
->pluck('ulasan_bersih') // gunakan kolom yang sudah dipreproses
|
||||
->toArray();
|
||||
|
||||
// gabungkan semua ulasan jadi 1 string
|
||||
$text = strtolower(implode(" ", $negatif));
|
||||
$text = strtolower(implode(' ', $ulasanNegatif));
|
||||
$words = array_filter(preg_split('/\s+/', $text));
|
||||
|
||||
// token kata sederhana
|
||||
$words = array_filter(explode(" ", $text));
|
||||
// --- 5. Rule-based: hitung skor tiap kategori isu ---
|
||||
$issueSkor = [];
|
||||
$issueWords = []; // kata dominan per kategori
|
||||
|
||||
foreach ($this->issueRules as $kategori => $rule) {
|
||||
$skor = 0;
|
||||
$matchWords = [];
|
||||
|
||||
foreach ($rule['keywords'] as $kw) {
|
||||
$count = substr_count($text, $kw);
|
||||
if ($count > 0) {
|
||||
$skor += $count;
|
||||
$matchWords[$kw] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
$issueSkor[$kategori] = $skor;
|
||||
$issueWords[$kategori] = $matchWords;
|
||||
}
|
||||
|
||||
// Urutkan berdasarkan skor tertinggi
|
||||
arsort($issueSkor);
|
||||
|
||||
$totalSkorIsu = array_sum($issueSkor) ?: 1; // hindari division by zero
|
||||
|
||||
// --- 6. Isu utama (top 5) dengan persentase ---
|
||||
$isuUtama = [];
|
||||
foreach (array_slice($issueSkor, 0, 5, true) as $nama => $skor) {
|
||||
$isuUtama[] = [
|
||||
'nama' => $nama,
|
||||
'skor' => $skor,
|
||||
'persen' => round(($skor / $totalSkorIsu) * 100),
|
||||
'color' => $this->issueRules[$nama]['color'],
|
||||
'icon' => $this->issueRules[$nama]['icon'],
|
||||
];
|
||||
}
|
||||
|
||||
// --- 7. Isu dominan (ranking 1) ---
|
||||
$isuDominan = $isuUtama[0]['nama'] ?? 'Belum ada isu';
|
||||
$isuDominanPersen = $isuUtama[0]['persen'] ?? 0;
|
||||
|
||||
// --- 8. Kata kunci dominan (global, top 10, filter stopword) ---
|
||||
$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',
|
||||
];
|
||||
|
||||
// hitung frekuensi kata
|
||||
$freq = array_count_values($words);
|
||||
foreach ($stopwords as $sw) {
|
||||
unset($freq[$sw]);
|
||||
}
|
||||
// filter kata pendek (< 3 karakter)
|
||||
foreach (array_keys($freq) as $w) {
|
||||
if (mb_strlen($w) < 3) unset($freq[$w]);
|
||||
}
|
||||
|
||||
// urutkan kata paling sering
|
||||
arsort($freq);
|
||||
$kataDominan = array_slice($freq, 0, 10, true);
|
||||
|
||||
// ambil top 5 keyword
|
||||
$topKeywords = array_slice(array_keys($freq), 0, 5);
|
||||
// --- 9. Prioritas rekomendasi (top 3 isu) ---
|
||||
$prioritas = [];
|
||||
$rank = 1;
|
||||
foreach (array_slice($issueSkor, 0, 3, true) as $nama => $skor) {
|
||||
$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'],
|
||||
];
|
||||
}
|
||||
|
||||
// buat output card
|
||||
$isuUtama = $topKeywords[0] ?? "Belum ada isu";
|
||||
$kataDominan = implode(", ", $topKeywords);
|
||||
// --- 10. Saran perbaikan (top 3) ---
|
||||
$saranPerbaikan = [];
|
||||
foreach (array_slice($issueSkor, 0, 3, true) as $nama => $skor) {
|
||||
$saranPerbaikan[] = [
|
||||
'nama' => 'Perbaikan ' . $nama,
|
||||
'tip' => $this->issueRules[$nama]['saran']['tip'],
|
||||
'icon' => $this->issueRules[$nama]['icon'],
|
||||
];
|
||||
}
|
||||
|
||||
// saran otomatis sederhana
|
||||
$saran = "Perlu peningkatan layanan terkait: " . $isuUtama;
|
||||
// --- 11. Filter destinasi yang sedang aktif ---
|
||||
$destinasiAktif = $request->input('destinasi', 'Semua Destinasi');
|
||||
|
||||
$destinasiList = DB::table('hasil_analisis')
|
||||
->select('wisata')
|
||||
->distinct()
|
||||
->pluck('wisata');
|
||||
|
||||
|
||||
return view('dashboard', compact(
|
||||
'stats',
|
||||
'chartSentimen',
|
||||
'chartDestinasi',
|
||||
'totalPerWisata',
|
||||
'allText',
|
||||
'lastUpdate',
|
||||
'hasAnalisis',
|
||||
'wisataList',
|
||||
'hasil',
|
||||
'destinasiList' // ✅ TAMBAH INI
|
||||
));
|
||||
return view('rekomendasi.index', compact(
|
||||
'destinasiList',
|
||||
'destinasiAktif',
|
||||
'totalUlasan',
|
||||
'totalNegatif',
|
||||
'totalPositif',
|
||||
'totalNetral',
|
||||
'persenNegatif',
|
||||
'tingkatKepuasan',
|
||||
'labelKepuasan',
|
||||
'isuUtama',
|
||||
'isuDominan',
|
||||
'isuDominanPersen',
|
||||
'kataDominan',
|
||||
'saranPerbaikan',
|
||||
'prioritas',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ class UlasanController extends Controller
|
|||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$periodeList = DB::table('periode_analisis')->orderBy('id', 'desc')->get();
|
||||
$query = Ulasan::query();
|
||||
|
||||
// 🔍 SEARCH (ulasan + wisata)
|
||||
|
|
@ -29,87 +30,116 @@ public function index(Request $request)
|
|||
$query->where('wisata', 'LIKE', '%' . $request->wisata . '%');
|
||||
}
|
||||
|
||||
$mentah = $query->latest()->paginate(20)->withQueryString();
|
||||
|
||||
// OPTIONAL
|
||||
$preprocessing = PreprocessingData::latest()->paginate(30, ['*'], 'pre_page');
|
||||
|
||||
// 🔥 RINGKASAN DATA
|
||||
$totalUlasan = Ulasan::count();
|
||||
$totalWisata = Ulasan::distinct('wisata')->count('wisata');
|
||||
$lastUpdate = \App\Models\Ulasan::latest()->value('created_at');
|
||||
|
||||
return view('ulasan.index', compact(
|
||||
'mentah',
|
||||
'preprocessing',
|
||||
'totalUlasan',
|
||||
'totalWisata',
|
||||
'lastUpdate'
|
||||
));
|
||||
}
|
||||
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|mimes:csv,txt,xlsx,xls|max:10240'
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
|
||||
if (in_array($extension, ['xlsx', 'xls'])) {
|
||||
|
||||
$data = Excel::toArray(null, $file);
|
||||
$rows = $data[0] ?? [];
|
||||
unset($rows[0]);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (empty($row) || count($row) < 4) continue;
|
||||
|
||||
DB::table('ulasan')->insert([
|
||||
'wisata' => $row[0] ?? null,
|
||||
'rating' => isset($row[1]) ? (float) $row[1] : 0,
|
||||
'ulasan' => $row[2] ?? '',
|
||||
'tanggal' => isset($row[3]) ? date('Y-m-d H:i:s', strtotime($row[3])) : now(),
|
||||
'sentimen' => null,
|
||||
'reviewer' => 'anonymous',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
|
||||
$data = array_map(fn($line) => str_getcsv($line, ';'), file($file));
|
||||
unset($data[0]);
|
||||
|
||||
foreach ($data as $row) {
|
||||
if (empty($row) || count($row) < 4) continue;
|
||||
|
||||
DB::table('ulasan')->insert([
|
||||
'wisata' => $row[0] ?? null,
|
||||
'rating' => isset($row[1]) ? (float) $row[1] : 0,
|
||||
'ulasan' => $row[2] ?? '',
|
||||
'tanggal' => isset($row[3]) ? date('Y-m-d H:i:s', strtotime($row[3])) : now(),
|
||||
'sentimen' => null,
|
||||
'reviewer' => 'anonymous',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
// ← filter periode
|
||||
if ($request->filled('periode_id')) {
|
||||
$query->where('periode_id', $request->periode_id);
|
||||
}
|
||||
|
||||
return redirect()->route('ulasan.index')
|
||||
->with('success', 'Upload berhasil dan data masuk!');
|
||||
$mentah = $query->orderBy('tanggal', 'desc')->paginate(20)->withQueryString();
|
||||
|
||||
// OPTIONAL
|
||||
$preprocessing = PreprocessingData::latest()->paginate(20, ['*'], 'pre_page');
|
||||
|
||||
// 🔥 RINGKASAN DATA
|
||||
|
||||
$totalUlasan = Ulasan::when($request->filled('periode_id'), fn($q) => $q->where('periode_id', $request->periode_id))->count();
|
||||
$totalWisata = Ulasan::when($request->filled('periode_id'), fn($q) => $q->where('periode_id', $request->periode_id))->distinct('wisata')->count('wisata');
|
||||
$lastUpdate = Ulasan::when($request->filled('periode_id'), fn($q) => $q->where('periode_id', $request->periode_id))->latest()->value('created_at');
|
||||
|
||||
return view('ulasan.index', compact(
|
||||
'mentah',
|
||||
'totalUlasan',
|
||||
'totalWisata',
|
||||
'lastUpdate',
|
||||
'periodeList'
|
||||
));
|
||||
}
|
||||
|
||||
public function analisisData()
|
||||
{
|
||||
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
|
||||
$scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"';
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|mimes:csv,txt,xlsx,xls|max:10240'
|
||||
]);
|
||||
|
||||
shell_exec($pythonPath . " " . $scriptPath . " 2>&1");
|
||||
// ✅ OTOMATIS BUAT / AMBIL PERIODE BULAN INI
|
||||
$bulan = now()->month;
|
||||
$tahun = now()->year;
|
||||
$namaBulan = now()->translatedFormat('F Y'); // contoh: "Mei 2026"
|
||||
|
||||
return redirect()->route('ulasan.index')
|
||||
->with('success', 'Analisis berhasil dijalankan');
|
||||
$periode = DB::table('periode_analisis')
|
||||
->where('bulan', $bulan)
|
||||
->where('tahun', $tahun)
|
||||
->first();
|
||||
|
||||
if (!$periode) {
|
||||
$periodeId = DB::table('periode_analisis')->insertGetId([
|
||||
'nama' => $namaBulan,
|
||||
'bulan' => $bulan,
|
||||
'tahun' => $tahun,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
$periodeId = $periode->id;
|
||||
}
|
||||
|
||||
// ✅ PROSES FILE
|
||||
$file = $request->file('file');
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
|
||||
if (in_array($extension, ['xlsx', 'xls'])) {
|
||||
$data = Excel::toArray(null, $file);
|
||||
$rows = $data[0] ?? [];
|
||||
unset($rows[0]);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (empty($row) || count($row) < 4) continue;
|
||||
|
||||
DB::table('ulasan')->insert([
|
||||
'wisata' => $row[0] ?? null,
|
||||
'rating' => isset($row[1]) ? (float) $row[1] : 0,
|
||||
'ulasan' => $row[2] ?? '',
|
||||
'tanggal' => isset($row[3]) ? date('Y-m-d H:i:s', strtotime($row[3])) : now(),
|
||||
'sentimen' => null,
|
||||
'reviewer' => 'anonymous',
|
||||
'periode_id' => $periodeId, // ← tag periode
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$data = array_map(fn($line) => str_getcsv($line, ';'), file($file));
|
||||
unset($data[0]);
|
||||
|
||||
foreach ($data as $row) {
|
||||
if (empty($row) || count($row) < 4) continue;
|
||||
|
||||
DB::table('ulasan')->insert([
|
||||
'wisata' => $row[0] ?? null,
|
||||
'rating' => isset($row[1]) ? (float) $row[1] : 0,
|
||||
'ulasan' => $row[2] ?? '',
|
||||
'tanggal' => isset($row[3]) ? date('Y-m-d H:i:s', strtotime($row[3])) : now(),
|
||||
'sentimen' => null,
|
||||
'reviewer' => 'anonymous',
|
||||
'periode_id' => $periodeId, // ← tag periode
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('ulasan.index')
|
||||
->with('success', 'Upload berhasil! Data masuk ke periode ' . $namaBulan);
|
||||
}
|
||||
|
||||
public function analisisData()
|
||||
{
|
||||
$pythonPath = "C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
|
||||
$scriptPath = base_path('scraper/analisis.py');
|
||||
|
||||
$command = "\"$pythonPath\" \"$scriptPath\" 2>&1";
|
||||
$output = shell_exec($command);
|
||||
|
||||
return redirect()->route('analisis.index')
|
||||
->with('success', 'Analisis berhasil dijalankan');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EvaluasiModel extends Model
|
||||
{
|
||||
protected $table = 'evaluasi_model';
|
||||
|
||||
protected $fillable = [
|
||||
'precision',
|
||||
'recall',
|
||||
'f1_score',
|
||||
'accuracy',
|
||||
'tp',
|
||||
'tn',
|
||||
'fp',
|
||||
'fn'
|
||||
];
|
||||
}
|
||||
|
|
@ -2,19 +2,18 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HasilAnalisis extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'hasil_analisis';
|
||||
|
||||
protected $fillable = [
|
||||
'wisata',
|
||||
'ulasan_terolah',
|
||||
'ulasan_asli', // ← fix dari ulasan_terolah
|
||||
'ulasan_bersih', // ← tambah ini
|
||||
'hasil_preprocessing',
|
||||
'sentimen',
|
||||
'probabilitas'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ public function register(): void
|
|||
public function boot(): void
|
||||
{
|
||||
Paginator::useTailwind();
|
||||
\Carbon\Carbon::setLocale('id');
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => 'Asia/Jakarta',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@
|
|||
Berikut adalah hasil evaluasi model dan detail analisis sentimen terhadap ulasan wisata.
|
||||
</p>
|
||||
</div>
|
||||
<!-- <div class="text-sm text-gray-500">
|
||||
{{ now()->format('d F Y - H:i') }}
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
{{-- ================= METRICS ================= --}}
|
||||
|
|
@ -19,28 +16,28 @@
|
|||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">Precision</p>
|
||||
<h3 class="text-2xl font-bold text-blue-600">
|
||||
{{ $evaluasi->precision ?? 0 }}
|
||||
{{ number_format($evaluasi->precision ?? 0, 2) }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">Recall</p>
|
||||
<h3 class="text-2xl font-bold text-green-600">
|
||||
{{ $evaluasi->recall ?? 0 }}
|
||||
{{ number_format($evaluasi->recall ?? 0, 2) }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">F1 Score</p>
|
||||
<h3 class="text-2xl font-bold text-purple-600">
|
||||
{{ $evaluasi->f1_score ?? 0 }}
|
||||
{{ number_format($evaluasi->f1_score ?? 0, 2) }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">Akurasi</p>
|
||||
<h3 class="text-2xl font-bold text-indigo-600">
|
||||
{{ $evaluasi->accuracy ?? 0 }}
|
||||
{{ number_format($evaluasi->accuracy ?? 0, 2) }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
|
@ -66,8 +63,8 @@
|
|||
<tr>
|
||||
<th class="bg-gray-50">Positif</th>
|
||||
<td class="bg-green-100">{{ $evaluasi->tp ?? 0 }}</td>
|
||||
<td class="bg-red-100">{{ $evaluasi->fn ?? 0 }}</td>
|
||||
<td class="bg-yellow-100">0</td>
|
||||
<td class="bg-red-100">{{ $evaluasi->fp ?? 0 }}</td>
|
||||
<td class="bg-yellow-100">{{ $evaluasi->fn ?? 0 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="bg-gray-50">Negatif</th>
|
||||
|
|
@ -96,29 +93,28 @@
|
|||
<th>Precision</th>
|
||||
<th>Recall</th>
|
||||
<th>F1</th>
|
||||
<th>Support</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-green-600 font-semibold">Positif</td>
|
||||
<td>{{ $evaluasi->precision ?? 0 }}</td>
|
||||
<td>{{ $evaluasi->recall ?? 0 }}</td>
|
||||
<td>{{ $evaluasi->f1_score ?? 0 }}</td>
|
||||
<td>{{ number_format($evaluasi->precision ?? 0, 2) }}</td>
|
||||
<td>{{ number_format($evaluasi->recall ?? 0, 2) }}</td>
|
||||
<td>{{ number_format($evaluasi->f1_score ?? 0, 2) }}</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-red-600 font-semibold">Negatif</td>
|
||||
<td>{{ $evaluasi->precision ?? 0 }}</td>
|
||||
<td>{{ $evaluasi->recall ?? 0 }}</td>
|
||||
<td>{{ $evaluasi->f1_score ?? 0 }}</td>
|
||||
<td>{{ number_format($evaluasi->precision ?? 0, 2) }}</td>
|
||||
<td>{{ number_format($evaluasi->recall ?? 0, 2) }}</td>
|
||||
<td>{{ number_format($evaluasi->f1_score ?? 0, 2) }}</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-yellow-600 font-semibold">Netral</td>
|
||||
<td>{{ $evaluasi->precision ?? 0 }}</td>
|
||||
<td>{{ $evaluasi->recall ?? 0 }}</td>
|
||||
<td>{{ $evaluasi->f1_score ?? 0 }}</td>
|
||||
<td>{{ number_format($evaluasi->precision ?? 0, 2) }}</td>
|
||||
<td>{{ number_format($evaluasi->recall ?? 0, 2) }}</td>
|
||||
<td>{{ number_format($evaluasi->f1_score ?? 0, 2) }}</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -128,25 +124,23 @@
|
|||
</div>
|
||||
|
||||
{{-- ================= FILTER ================= --}}
|
||||
<form id="filter-form" class="flex items-center gap-3">
|
||||
<form id="filter-form" class="flex items-center gap-3">
|
||||
|
||||
<input type="hidden" name="tab" value="analisis">
|
||||
<input type="hidden" name="tab" value="analisis">
|
||||
|
||||
<select name="wisata" class="border rounded px-3 py-2">
|
||||
<option value="">Semua Destinasi</option>
|
||||
<select name="wisata" class="border rounded px-3 py-2">
|
||||
<option value="">Semua Destinasi</option>
|
||||
<option value="Pantai Papuma" {{ request('wisata') == 'Pantai Papuma' ? 'selected' : '' }}>Pantai Papuma</option>
|
||||
<option value="Pantai Watu Ulo" {{ request('wisata') == 'Pantai Watu Ulo' ? 'selected' : '' }}>Pantai Watu Ulo</option>
|
||||
<option value="Teluk Love" {{ request('wisata') == 'Teluk Love' ? 'selected' : '' }}>Teluk Love</option>
|
||||
<option value="Wisata Kebun Teh Gunung Gambir" {{ request('wisata') == 'Wisata Kebun Teh Gunung Gambir' ? 'selected' : '' }}>Wisata Kebun Teh Gunung Gambir</option>
|
||||
</select>
|
||||
|
||||
@foreach($destinasiList as $d)
|
||||
<option value="{{ $d }}" {{ request('wisata') == $d ? 'selected' : '' }}>
|
||||
{{ $d }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
|
||||
Filter
|
||||
</button>
|
||||
|
||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
|
||||
Filter
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</form>
|
||||
|
||||
{{-- ================= TABLE ================= --}}
|
||||
<div class="bg-white rounded-xl shadow p-4">
|
||||
|
|
@ -158,102 +152,100 @@
|
|||
<tr>
|
||||
<th>No</th>
|
||||
<th>Wisata</th>
|
||||
<th>Ulasan</th>
|
||||
<th>Ulasan Asli</th>
|
||||
<th>Ulasan Bersih</th>
|
||||
<th>Sentimen</th>
|
||||
<th>Probabilitas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@forelse($hasil as $index => $item)
|
||||
@forelse($hasil as $item)
|
||||
<tr class="border-b">
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ ($hasil->currentPage() - 1) * $hasil->perPage() + $loop->iteration }}</td>
|
||||
<td>{{ $item->wisata }}</td>
|
||||
<td>{{ Str::limit($item->ulasan_terolah, 80) }}</td>
|
||||
|
||||
<td class="text-gray-500">{{ Str::limit($item->ulasan_asli ?? '-', 80) }}</td>
|
||||
<td>{{ Str::limit($item->hasil_preprocessing ?? '-', 80) }}</td>
|
||||
<td>
|
||||
<span class="
|
||||
px-2 py-1 rounded text-xs
|
||||
{{ $item->sentimen == 'Positif' ? 'bg-green-100 text-green-600' : '' }}
|
||||
{{ $item->sentimen == 'Negatif' ? 'bg-red-100 text-red-600' : '' }}
|
||||
{{ $item->sentimen == 'Netral' ? 'bg-yellow-100 text-yellow-600' : '' }}
|
||||
<span class="px-2 py-1 rounded text-xs
|
||||
{{ strtolower($item->sentimen) == 'positif' ? 'bg-green-100 text-green-600' : '' }}
|
||||
{{ strtolower($item->sentimen) == 'negatif' ? 'bg-red-100 text-red-600' : '' }}
|
||||
{{ strtolower($item->sentimen) == 'netral' ? 'bg-yellow-100 text-yellow-600' : '' }}
|
||||
">
|
||||
{{ $item->sentimen }}
|
||||
{{ ucfirst($item->sentimen) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>{{ $item->probabilitas ?? '-' }}</td>
|
||||
<td>{{ number_format($item->probabilitas ?? 0, 2) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-gray-500">
|
||||
Tidak ada data
|
||||
</td>
|
||||
<td colspan="6" class="text-center py-4 text-gray-500">Tidak ada data</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- PAGINATION --}}
|
||||
<div class="mt-6 flex items-center justify-between text-sm text-gray-500">
|
||||
{{-- ================= PAGINATION ================= --}}
|
||||
@if($hasil->hasPages())
|
||||
@php
|
||||
$current = $hasil->currentPage();
|
||||
$last = $hasil->lastPage();
|
||||
$query = http_build_query(request()->except('page'));
|
||||
// Window 2 halaman kiri & kanan dari current
|
||||
$window = collect(range(max(1, $current - 2), min($last, $current + 2)));
|
||||
@endphp
|
||||
|
||||
{{-- INFO --}}
|
||||
<div>
|
||||
Menampilkan {{ $hasil->firstItem() }} - {{ $hasil->lastItem() }}
|
||||
dari {{ $hasil->total() }} data
|
||||
</div>
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-gray-600">
|
||||
|
||||
{{-- PAGINATION --}}
|
||||
{{-- PAGINATION --}}
|
||||
<div class="flex items-center gap-1">
|
||||
{{-- Info --}}
|
||||
<p>Menampilkan {{ $hasil->firstItem() }} - {{ $hasil->lastItem() }} dari {{ $hasil->total() }} data</p>
|
||||
|
||||
{{-- PREV --}}
|
||||
@if ($hasil->onFirstPage())
|
||||
<span class="px-3 py-1 rounded border text-gray-300">‹</span>
|
||||
@else
|
||||
<a href="{{ $hasil->appends(request()->query())->previousPageUrl() }}"
|
||||
class="px-3 py-1 rounded border hover:bg-gray-100">‹</a>
|
||||
@endif
|
||||
{{-- Tombol --}}
|
||||
<div class="flex items-center gap-1">
|
||||
|
||||
{{-- PAGE NUMBERS --}}
|
||||
@foreach ($hasil->getUrlRange(1, $hasil->lastPage()) as $page => $url)
|
||||
{{-- « --}}
|
||||
@if($hasil->onFirstPage())
|
||||
<span class="px-3 py-1 rounded border bg-gray-100 text-gray-400 cursor-not-allowed">«</span>
|
||||
@else
|
||||
<a href="{{ $hasil->previousPageUrl() }}&{{ $query }}" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">«</a>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$query = request()->query();
|
||||
$query['page'] = $page;
|
||||
$customUrl = url()->current() . '?' . http_build_query($query);
|
||||
@endphp
|
||||
{{-- Halaman 1 (jika tidak ada di window) --}}
|
||||
@if(!$window->contains(1))
|
||||
<a href="{{ $hasil->url(1) }}&{{ $query }}" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">1</a>
|
||||
@if($window->min() > 2)
|
||||
<span class="px-2 py-1 text-gray-400">...</span>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if ($page == $hasil->currentPage())
|
||||
<span class="px-3 py-1 rounded bg-blue-600 text-white">
|
||||
{{ $page }}
|
||||
</span>
|
||||
{{-- Window --}}
|
||||
@foreach($window as $page)
|
||||
@if($page == $current)
|
||||
<span class="px-3 py-1 rounded border bg-blue-600 text-white font-semibold">{{ $page }}</span>
|
||||
@else
|
||||
<a href="{{ $hasil->url($page) }}&{{ $query }}" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">{{ $page }}</a>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
@elseif ($page <= 3 || $page > $hasil->lastPage()-3 || abs($page - $hasil->currentPage()) <= 1)
|
||||
{{-- Halaman terakhir (jika tidak ada di window) --}}
|
||||
@if(!$window->contains($last))
|
||||
@if($window->max() < $last - 1)
|
||||
<span class="px-2 py-1 text-gray-400">...</span>
|
||||
@endif
|
||||
<a href="{{ $hasil->url($last) }}&{{ $query }}" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">{{ $last }}</a>
|
||||
@endif
|
||||
|
||||
<a href="{{ $customUrl }}"
|
||||
class="px-3 py-1 rounded border hover:bg-gray-100">
|
||||
{{ $page }}
|
||||
</a>
|
||||
{{-- » --}}
|
||||
@if($hasil->hasMorePages())
|
||||
<a href="{{ $hasil->nextPageUrl() }}&{{ $query }}" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">»</a>
|
||||
@else
|
||||
<span class="px-3 py-1 rounded border bg-gray-100 text-gray-400 cursor-not-allowed">»</span>
|
||||
@endif
|
||||
|
||||
@elseif ($page == 4 || $page == $hasil->lastPage()-3)
|
||||
<span class="px-2">...</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
|
||||
{{-- NEXT --}}
|
||||
@if ($hasil->hasMorePages())
|
||||
<a href="{{ $hasil->appends(request()->query())->nextPageUrl() }}"
|
||||
class="px-3 py-1 rounded border hover:bg-gray-100">›</a>
|
||||
@else
|
||||
<span class="px-3 py-1 rounded border text-gray-300">›</span>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -11,264 +11,243 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<div x-data="{ tab: 'dashboard' }" class="mb-6">
|
||||
|
||||
<!-- TAB -->
|
||||
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
||||
|
||||
<button @click="tab='dashboard'"
|
||||
:class="tab==='dashboard'
|
||||
? 'bg-white text-blue-700 shadow'
|
||||
: 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Dashboard Sentara
|
||||
</button>
|
||||
|
||||
<button @click="tab='analisis'"
|
||||
:class="tab==='analisis'
|
||||
? 'bg-white text-blue-700 shadow'
|
||||
: 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Hasil Analisis Sentimen
|
||||
</button>
|
||||
|
||||
<button @click="tab='rekomendasi'"
|
||||
:class="tab==='rekomendasi'
|
||||
? 'bg-white text-blue-700 shadow'
|
||||
: 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Rekomendasi Layanan
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'dashboard'">
|
||||
{{-- Statistik Cards --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Total Ulasan</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-gray-900">
|
||||
{{ $stats['total'] }}
|
||||
</h3>
|
||||
{{-- DROPDOWN PERIODE --}}
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-gray-600 font-medium">Periode Analisis:</span>
|
||||
<form method="GET" action="{{ route('dashboard') }}" id="periode-form">
|
||||
<input type="hidden" name="tab" id="tab-hidden" value="{{ request('tab', 'dashboard') }}">
|
||||
<input type="hidden" name="wisata" value="{{ request('wisata') }}">
|
||||
<input type="hidden" name="destinasi" value="{{ request('destinasi') }}">
|
||||
<select name="periode_id" onchange="document.getElementById('periode-form').submit()"
|
||||
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm">
|
||||
@foreach($periodeList as $p)
|
||||
<option value="{{ $p->id }}" {{ ($periodeAktif && $periodeAktif->id == $p->id) ? 'selected' : '' }}>
|
||||
{{ $p->nama }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Positif</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-green-600">
|
||||
{{ $stats['positif_persen'] }}%
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Negatif</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-red-600">
|
||||
{{ $stats['negatif_persen'] }}%
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Netral</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-yellow-500">
|
||||
{{ $stats['netral_persen'] }}%
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@if($periodeAktif)
|
||||
<span class="text-xs text-gray-400">
|
||||
Data dianalisis: {{ \Carbon\Carbon::parse($periodeAktif->created_at)->format('d M Y') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Chart Section --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<div x-data="{ tab: new URLSearchParams(window.location.search).get('tab') || 'dashboard' }" class="mb-6">
|
||||
|
||||
<!-- Pie Chart -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Distribusi Sentimen</h3>
|
||||
<div style="height:300px;">
|
||||
<canvas id="pieChart"></canvas>
|
||||
</div>
|
||||
<!-- TAB -->
|
||||
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
||||
<button @click="tab='dashboard'; document.getElementById('tab-hidden').value='dashboard'"
|
||||
:class="tab==='dashboard' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Dashboard Sentara
|
||||
</button>
|
||||
<button @click="tab='analisis'; document.getElementById('tab-hidden').value='analisis'"
|
||||
:class="tab==='analisis' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Hasil Analisis Sentimen
|
||||
</button>
|
||||
<button @click="tab='rekomendasi'; document.getElementById('tab-hidden').value='rekomendasi'"
|
||||
:class="tab==='rekomendasi' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Rekomendasi Layanan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bar Chart -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Sentimen per Destinasi</h3>
|
||||
<div style="height:300px;">
|
||||
<canvas id="barChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ================= DASHBOARD ================= -->
|
||||
<div x-show="tab === 'dashboard'">
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Section Bawah --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Total Data per Destinasi -->
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">
|
||||
Total Data per Destinasi
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4 text-sm text-gray-600">
|
||||
@foreach($totalPerWisata as $item)
|
||||
<div class="flex justify-between border-b pb-3">
|
||||
<span>{{ $item->wisata }}</span>
|
||||
<span class="font-semibold text-gray-800">
|
||||
{{ $item->total }} ulasan
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
{{-- Statistik Cards --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Total Ulasan</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-gray-900">{{ $stats['total'] }}</h3>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Positif</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-green-600">{{ $stats['positif_persen'] }}%</h3>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Negatif</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-red-600">{{ $stats['negatif_persen'] }}%</h3>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Netral</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-yellow-500">{{ $stats['netral_persen'] }}%</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WORD CLOUD -->
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">
|
||||
Word Cloud
|
||||
</h3>
|
||||
<div id="wordCloud" class="w-full h-[240px] overflow-hidden"></div>
|
||||
</div>
|
||||
{{-- Chart Section --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
|
||||
</div>
|
||||
{{-- PIE CHART --}}
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Distribusi Sentimen</h3>
|
||||
<div style="height:300px;">
|
||||
<canvas id="pieChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- BAR CHART - PURE HTML --}}
|
||||
{{-- BAR CHART - PURE HTML VERTICAL --}}
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Sentimen per Destinasi</h3>
|
||||
{{-- Legend --}}
|
||||
<div class="flex gap-4 mb-4 text-xs">
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-blue-500 inline-block"></span> Positif</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-red-400 inline-block"></span> Negatif</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-yellow-400 inline-block"></span> Netral</span>
|
||||
</div>
|
||||
@php
|
||||
$maxVal = 1;
|
||||
foreach($chartDestinasi as $item) {
|
||||
$maxVal = max($maxVal, $item->positif, $item->negatif, $item->netral);
|
||||
}
|
||||
@endphp
|
||||
<div class="flex items-end gap-4 overflow-x-auto" style="height:220px;">
|
||||
@foreach($chartDestinasi as $item)
|
||||
@php
|
||||
$hPos = round(($item->positif / $maxVal) * 180);
|
||||
$hNeg = round(($item->negatif / $maxVal) * 180);
|
||||
$hNet = round(($item->netral / $maxVal) * 180);
|
||||
$label = strlen($item->wisata) > 12
|
||||
? substr($item->wisata, 0, 12) . '...'
|
||||
: $item->wisata;
|
||||
@endphp
|
||||
<div class="flex flex-col items-center flex-1 min-w-[60px]">
|
||||
<div class="flex items-end gap-1 w-full justify-center" style="height:180px;">
|
||||
{{-- Positif --}}
|
||||
<div class="relative group flex-1" style="height:{{ $hPos }}px; background:#3B82F6; border-radius:4px 4px 0 0; min-width:10px;">
|
||||
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-blue-600 font-semibold">{{ $item->positif }}</span>
|
||||
</div>
|
||||
{{-- Negatif --}}
|
||||
<div class="relative group flex-1" style="height:{{ $hNeg }}px; background:#F87171; border-radius:4px 4px 0 0; min-width:10px;">
|
||||
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-red-500 font-semibold">{{ $item->negatif }}</span>
|
||||
</div>
|
||||
{{-- Netral --}}
|
||||
<div class="relative group flex-1" style="height:{{ $hNet }}px; background:#FBBF24; border-radius:4px 4px 0 0; min-width:10px;">
|
||||
<span class="absolute -top-5 left-1/2 -translate-x-1/2 text-xs text-yellow-500 font-semibold">{{ $item->netral }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 text-center mt-2 leading-tight" style="max-width:70px; word-break:break-word;">{{ $label }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- ================= ANALISIS ================= -->
|
||||
<div x-show="tab === 'analisis'" id="analisis-content">
|
||||
@include('analisis.index')
|
||||
</div>
|
||||
|
||||
<!-- ================= REKOMENDASI ================= -->
|
||||
<div x-show="tab === 'rekomendasi'" id="rekomendasi-content">
|
||||
@include('rekomendasi.index')
|
||||
</div>
|
||||
|
||||
{{-- Section Bawah --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">Total Data per Destinasi</h3>
|
||||
<div class="space-y-4 text-sm text-gray-600">
|
||||
@foreach($totalPerWisata as $item)
|
||||
<div class="flex justify-between border-b pb-3">
|
||||
<span>{{ $item->wisata }}</span>
|
||||
<span class="font-semibold text-gray-800">{{ $item->total }} ulasan</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">Word Cloud</h3>
|
||||
<div id="wordCloud" class="w-full h-[240px] overflow-hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ================= ANALISIS ================= -->
|
||||
<div x-show="tab === 'analisis'" id="analisis-content">
|
||||
@include('analisis.index')
|
||||
</div>
|
||||
|
||||
<!-- ================= REKOMENDASI ================= -->
|
||||
<div x-show="tab === 'rekomendasi'" id="rekomendasi-content">
|
||||
@include('rekomendasi.index')
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{{-- ========================= --}}
|
||||
{{-- SCRIPT SECTION --}}
|
||||
{{-- ========================= --}}
|
||||
|
||||
{{-- SCRIPTS --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/wordcloud2.js/1.1.2/wordcloud2.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// =============================
|
||||
// PIE CHART
|
||||
// =============================
|
||||
const pieData = JSON.parse('{!! json_encode($chartSentimen) !!}');
|
||||
|
||||
const pieLabels = pieData.map(item => item.sentimen);
|
||||
const pieValues = pieData.map(item => item.total);
|
||||
|
||||
new Chart(document.getElementById('pieChart'), {
|
||||
const pieCanvas = document.getElementById('pieChart');
|
||||
if (window.pieChartInstance) window.pieChartInstance.destroy();
|
||||
window.pieChartInstance = new Chart(pieCanvas, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: pieLabels,
|
||||
labels: pieData.map(i => i.sentimen),
|
||||
datasets: [{
|
||||
data: pieValues
|
||||
data: pieData.map(i => i.total),
|
||||
backgroundColor: ['#3B82F6', '#EF4444', '#F59E0B']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false
|
||||
}
|
||||
});
|
||||
|
||||
// =============================
|
||||
// BAR CHART
|
||||
// =============================
|
||||
const barData = JSON.parse('{!! json_encode($chartDestinasi) !!}');
|
||||
|
||||
const labelsBar = barData.map(item => item.wisata);
|
||||
const positif = barData.map(item => item.positif);
|
||||
const negatif = barData.map(item => item.negatif);
|
||||
const netral = barData.map(item => item.netral);
|
||||
|
||||
new Chart(document.getElementById('barChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labelsBar,
|
||||
datasets: [
|
||||
{ label: 'Positif', data: positif },
|
||||
{ label: 'Negatif', data: negatif },
|
||||
{ label: 'Netral', data: netral }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// =============================
|
||||
// WORD CLOUD
|
||||
// =============================
|
||||
const text = `{!! $allText !!}`;
|
||||
|
||||
if(text.length > 0){
|
||||
const words = text.split(" ");
|
||||
|
||||
const text = `{!! addslashes($allText) !!}`;
|
||||
if (text.length > 0) {
|
||||
const wordCount = {};
|
||||
words.forEach(word => {
|
||||
if(word.length > 3){
|
||||
wordCount[word] = (wordCount[word] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const list = Object.entries(wordCount);
|
||||
|
||||
WordCloud(document.getElementById('wordCloud'), {
|
||||
list: list
|
||||
text.split(" ").forEach(word => {
|
||||
if (word.length > 3) wordCount[word] = (wordCount[word] || 0) + 1;
|
||||
});
|
||||
WordCloud(document.getElementById('wordCloud'), { list: Object.entries(wordCount) });
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const tab = new URLSearchParams(window.location.search).get('tab');
|
||||
|
||||
if (tab === 'analisis') {
|
||||
showTab('analisis');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const container = document.getElementById('analisis-content');
|
||||
|
||||
// ================= FILTER AJAX =================
|
||||
// FILTER AJAX
|
||||
document.addEventListener('submit', function(e){
|
||||
if(e.target.id === 'filter-form'){
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const params = new URLSearchParams(formData).toString();
|
||||
|
||||
const params = new URLSearchParams(new FormData(e.target)).toString();
|
||||
fetch('/dashboard?' + params)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const newContent = doc.getElementById('analisis-content');
|
||||
container.innerHTML = newContent.innerHTML;
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ================= PAGINATION AJAX =================
|
||||
// PAGINATION AJAX
|
||||
document.addEventListener('click', function(e){
|
||||
const link = e.target.closest('a');
|
||||
|
||||
if(link && link.href.includes('page=')){
|
||||
e.preventDefault();
|
||||
|
||||
fetch(link.href)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const newContent = doc.getElementById('analisis-content');
|
||||
container.innerHTML = newContent.innerHTML;
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
|
@ -1,3 +1,18 @@
|
|||
@php
|
||||
$destinasiList ??= collect([]);
|
||||
$totalNegatif ??= 0;
|
||||
$totalUlasan ??= 0;
|
||||
$persenNegatif ??= 0;
|
||||
$tingkatKepuasan ??= 0;
|
||||
$labelKepuasan ??= 'Kurang';
|
||||
$isuDominan ??= '-';
|
||||
$isuDominanPersen ??= 0;
|
||||
$isuUtama ??= [];
|
||||
$kataDominan ??= [];
|
||||
$saranPerbaikan ??= [];
|
||||
$prioritas ??= [];
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
{{-- HEADER --}}
|
||||
|
|
@ -9,26 +24,29 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<select class="border rounded-lg px-3 py-2 text-sm">
|
||||
<option>Semua Destinasi</option>
|
||||
{{-- FILTER DESTINASI — submit ke /dashboard agar tab tidak reset --}}
|
||||
<form method="GET" action="{{ route('dashboard') }}" class="flex gap-3">
|
||||
<input type="hidden" name="tab" value="rekomendasi">
|
||||
<select name="destinasi" onchange="this.form.submit()"
|
||||
class="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||
<option value="">Semua Destinasi</option>
|
||||
<option value="Pantai Papuma" {{ request('destinasi') == 'Pantai Papuma' ? 'selected' : '' }}>Pantai Papuma</option>
|
||||
<option value="Pantai Watu Ulo" {{ request('destinasi') == 'Pantai Watu Ulo' ? 'selected' : '' }}>Pantai Watu Ulo</option>
|
||||
<option value="Teluk Love" {{ request('destinasi') == 'Teluk Love' ? 'selected' : '' }}>Teluk Love</option>
|
||||
<option value="Wisata Kebun Teh Gunung Gambir" {{ request('destinasi') == 'Wisata Kebun Teh Gunung Gambir' ? 'selected' : '' }}>Wisata Kebun Teh Gunung Gambir</option>
|
||||
</select>
|
||||
|
||||
<!-- <select class="border rounded-lg px-3 py-2 text-sm">
|
||||
<option>2020 - 2025</option>
|
||||
</select> -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- SUMMARY --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-5">
|
||||
{{-- SUMMARY CARDS --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
|
||||
<div class="bg-red-100 text-red-500 p-3 rounded-full text-xl">😟</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Total Ulasan Negatif</p>
|
||||
<h3 class="text-xl font-bold">312</h3>
|
||||
<p class="text-xs text-red-500">25,06% dari total</p>
|
||||
<h3 class="text-xl font-bold">{{ $totalNegatif }}</h3>
|
||||
<p class="text-xs text-red-500">{{ $persenNegatif }}% dari total</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -36,9 +54,12 @@
|
|||
<div class="bg-green-100 text-green-600 p-3 rounded-full text-xl">😊</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Tingkat Kepuasan</p>
|
||||
<h3 class="text-xl font-bold">68%</h3>
|
||||
<span class="text-xs bg-yellow-100 text-yellow-700 px-2 py-1 rounded">
|
||||
Sedang
|
||||
<h3 class="text-xl font-bold">{{ $tingkatKepuasan }}%</h3>
|
||||
<span class="text-xs px-2 py-1 rounded
|
||||
{{ $labelKepuasan === 'Baik' ? 'bg-green-100 text-green-700' :
|
||||
($labelKepuasan === 'Sedang' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700') }}">
|
||||
{{ $labelKepuasan }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -47,20 +68,11 @@
|
|||
<div class="bg-purple-100 text-purple-600 p-3 rounded-full text-xl">❗</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Isu Dominan</p>
|
||||
<h3 class="text-xl font-bold">Kebersihan</h3>
|
||||
<p class="text-xs text-gray-500">45% dari total isu</p>
|
||||
<h3 class="text-xl font-bold">{{ $isuDominan }}</h3>
|
||||
<p class="text-xs text-gray-500">{{ $isuDominanPersen }}% dari total isu</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
|
||||
<div class="bg-blue-100 text-blue-600 p-3 rounded-full text-xl">📅</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Periode Data</p>
|
||||
<h3 class="text-xl font-bold">2020 - 2025</h3>
|
||||
<p class="text-xs text-gray-500">6 tahun data</p>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
{{-- GRID ANALISIS --}}
|
||||
|
|
@ -70,158 +82,120 @@
|
|||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Isu Utama (Top 5)</h3>
|
||||
|
||||
@php
|
||||
$isu = [
|
||||
['nama'=>'Kebersihan','persen'=>45,'warna'=>'red'],
|
||||
['nama'=>'Parkir','persen'=>30,'warna'=>'orange'],
|
||||
['nama'=>'Harga / Tiket','persen'=>15,'warna'=>'yellow'],
|
||||
['nama'=>'Fasilitas','persen'=>6,'warna'=>'green'],
|
||||
['nama'=>'Keramaian','persen'=>4,'warna'=>'blue'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach($isu as $i => $item)
|
||||
@forelse ($isuUtama as $i => $isu)
|
||||
@php
|
||||
$warnaClass = match($isu['color']) {
|
||||
'red' => 'bg-red-500',
|
||||
'orange' => 'bg-orange-500',
|
||||
'yellow' => 'bg-yellow-400',
|
||||
'green' => 'bg-green-500',
|
||||
default => 'bg-blue-500',
|
||||
};
|
||||
@endphp
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span>{{ $i+1 }}. {{ $item['nama'] }}</span>
|
||||
<span>{{ $item['persen'] }}%</span>
|
||||
<span>{{ $i + 1 }}. {{ $isu['nama'] }}</span>
|
||||
<span class="font-semibold">{{ $isu['persen'] }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 h-2 rounded">
|
||||
<div class="h-2 rounded
|
||||
@if($item['warna'] == 'red') bg-red-500
|
||||
@elseif($item['warna'] == 'orange') bg-orange-500
|
||||
@elseif($item['warna'] == 'yellow') bg-yellow-500
|
||||
@elseif($item['warna'] == 'green') bg-green-500
|
||||
@else bg-blue-500
|
||||
@endif"
|
||||
style="width: {{ $item['persen'] }}%">
|
||||
</div>
|
||||
<div class="h-2 rounded {{ $warnaClass }}" style="width: {{ $isu['persen'] }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@empty
|
||||
<p class="text-sm text-gray-400 text-center py-4">Belum ada data isu.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- KEYWORD --}}
|
||||
{{-- KATA KUNCI DOMINAN --}}
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Kata Kunci Dominan</h3>
|
||||
|
||||
@php
|
||||
$keywords = [
|
||||
['kotor',15],
|
||||
['parkir',10],
|
||||
['mahal',7],
|
||||
['sampah',6],
|
||||
['toilet',5],
|
||||
];
|
||||
@endphp
|
||||
@php $maxFreq = !empty($kataDominan) ? max($kataDominan) : 1; @endphp
|
||||
|
||||
@foreach($keywords as $k)
|
||||
@forelse (array_slice($kataDominan, 0, 5, true) as $kata => $jumlah)
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>{{ $k[0] }}</span>
|
||||
<span>{{ $k[1] }}x</span>
|
||||
<span>{{ $kata }}</span>
|
||||
<span class="font-semibold text-blue-600">{{ $jumlah }}x</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 h-2 rounded mt-1">
|
||||
<div class="bg-blue-500 h-2 rounded"
|
||||
style="width: {{ $k[1]*5 }}%"></div>
|
||||
<div class="bg-blue-500 h-2 rounded" style="width: {{ round(($jumlah / $maxFreq) * 100) }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@empty
|
||||
<p class="text-sm text-gray-400 text-center py-4">Belum ada kata kunci.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- SARAN --}}
|
||||
{{-- SARAN PERBAIKAN --}}
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Saran Perbaikan</h3>
|
||||
|
||||
<div class="space-y-4 text-sm">
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div class="text-green-500 text-xl">🧹</div>
|
||||
<div>
|
||||
<p class="font-semibold">Perbaikan Kebersihan</p>
|
||||
<p class="text-gray-600">
|
||||
Tambah tempat sampah & jadwal pembersihan rutin.
|
||||
</p>
|
||||
@forelse ($saranPerbaikan as $saran)
|
||||
<div class="flex gap-3">
|
||||
<div class="text-xl">{{ $saran['icon'] }}</div>
|
||||
<div>
|
||||
<p class="font-semibold">{{ $saran['nama'] }}</p>
|
||||
<p class="text-gray-600">{{ $saran['tip'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div class="text-orange-500 text-xl">🚗</div>
|
||||
<div>
|
||||
<p class="font-semibold">Penataan Parkir</p>
|
||||
<p class="text-gray-600">
|
||||
Sistem parkir lebih rapi dan transparan.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div class="text-yellow-500 text-xl">🏷️</div>
|
||||
<div>
|
||||
<p class="font-semibold">Evaluasi Harga</p>
|
||||
<p class="text-gray-600">
|
||||
Penyesuaian harga tiket agar lebih terjangkau.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@empty
|
||||
<p class="text-sm text-gray-400 text-center py-4">Belum ada saran.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- PRIORITAS --}}
|
||||
{{-- PRIORITAS REKOMENDASI --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
{{-- CARD 1 --}}
|
||||
<div class="border border-red-300 rounded-xl p-5 bg-white">
|
||||
<h4 class="font-semibold text-red-600 mb-2">Prioritas 1</h4>
|
||||
<h3 class="text-lg font-bold mb-3">Kebersihan</h3>
|
||||
@forelse ($prioritas as $p)
|
||||
@php
|
||||
$border = match($p['color']) {
|
||||
'red' => 'border-red-300',
|
||||
'orange' => 'border-orange-300',
|
||||
'yellow' => 'border-yellow-300',
|
||||
'green' => 'border-green-300',
|
||||
default => 'border-blue-300',
|
||||
};
|
||||
$title = match($p['color']) {
|
||||
'red' => 'text-red-600',
|
||||
'orange' => 'text-orange-500',
|
||||
'yellow' => 'text-yellow-600',
|
||||
'green' => 'text-green-600',
|
||||
default => 'text-blue-600',
|
||||
};
|
||||
$dampak = match($p['color']) {
|
||||
'red' => 'bg-red-100 text-red-600',
|
||||
'orange' => 'bg-orange-100 text-orange-600',
|
||||
'yellow' => 'bg-yellow-100 text-yellow-600',
|
||||
'green' => 'bg-green-100 text-green-700',
|
||||
default => 'bg-blue-100 text-blue-600',
|
||||
};
|
||||
@endphp
|
||||
<div class="border {{ $border }} rounded-xl p-5 bg-white">
|
||||
<h4 class="font-semibold {{ $title }} mb-2">Prioritas {{ $p['rank'] }}</h4>
|
||||
<h3 class="text-lg font-bold mb-3">{{ $p['nama'] }}</h3>
|
||||
|
||||
<ul class="text-sm space-y-2">
|
||||
<li>✔ Tambah tempat sampah</li>
|
||||
<li>✔ Jadwal pembersihan rutin</li>
|
||||
<li>✔ Petugas kebersihan tambahan</li>
|
||||
</ul>
|
||||
<ul class="text-sm space-y-2 mb-4">
|
||||
@foreach ($p['actions'] as $action)
|
||||
<li class="flex items-center gap-2">
|
||||
<span class="text-blue-500">✔</span> {{ $action }}
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 text-xs bg-red-100 text-red-600 p-2 rounded">
|
||||
Dampak: Kepuasan meningkat signifikan
|
||||
<div class="text-xs {{ $dampak }} p-2 rounded">
|
||||
Dampak: {{ $p['dampak'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- CARD 2 --}}
|
||||
<div class="border border-orange-300 rounded-xl p-5 bg-white">
|
||||
<h4 class="font-semibold text-orange-600 mb-2">Prioritas 2</h4>
|
||||
<h3 class="text-lg font-bold mb-3">Parkir</h3>
|
||||
|
||||
<ul class="text-sm space-y-2">
|
||||
<li>✔ Penataan area parkir</li>
|
||||
<li>✔ Tarif transparan</li>
|
||||
<li>✔ Petugas lebih ramah</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 text-xs bg-orange-100 text-orange-600 p-2 rounded">
|
||||
Dampak: Mengurangi keluhan
|
||||
@empty
|
||||
<div class="col-span-3 text-center text-gray-400 text-sm py-6">
|
||||
Belum ada data rekomendasi. Pastikan analisis sentimen sudah dijalankan.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- CARD 3 --}}
|
||||
<div class="border border-yellow-300 rounded-xl p-5 bg-white">
|
||||
<h4 class="font-semibold text-yellow-600 mb-2">Prioritas 3</h4>
|
||||
<h3 class="text-lg font-bold mb-3">Harga</h3>
|
||||
|
||||
<ul class="text-sm space-y-2">
|
||||
<li>✔ Evaluasi harga tiket</li>
|
||||
<li>✔ Promo wisata</li>
|
||||
<li>✔ Diskon tertentu</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 text-xs bg-yellow-100 text-yellow-600 p-2 rounded">
|
||||
Dampak: Daya tarik meningkat
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,68 +5,56 @@
|
|||
<div class="max-w-7xl mx-auto px-4 md:px-0 space-y-6">
|
||||
|
||||
<!-- HEADER -->
|
||||
<h2 class="text-2xl font-bold text-gray-800">Data Ulasan</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold text-gray-800">Data Ulasan</h2>
|
||||
<div class="flex gap-3">
|
||||
<button class="border border-blue-500 text-blue-600 px-5 py-2.5 rounded-full hover:bg-blue-50 transition">
|
||||
Ambil Data Terbaru
|
||||
</button>
|
||||
<form action="{{ route('proses.analisis') }}" method="POST">
|
||||
@csrf
|
||||
<button class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded-full shadow-md transition">
|
||||
Proses Analisis
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- 🔥 UPLOAD + FORMAT + SUMMARY -->
|
||||
<!-- ============================= -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
</div>
|
||||
|
||||
<!-- UPLOAD + FORMAT + SUMMARY -->
|
||||
<div class="bg-white rounded-3xl shadow p-6 mb-6 mt-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 items-center">
|
||||
|
||||
<!-- UPLOAD -->
|
||||
<div class="border-2 border-dashed border-blue-300 rounded-xl p-6 text-center">
|
||||
|
||||
<div class="flex justify-center mb-3">
|
||||
<div class="bg-blue-100 text-blue-600 p-3 rounded-full text-xl">
|
||||
☁️
|
||||
</div>
|
||||
<div class="bg-blue-100 text-blue-600 p-3 rounded-full text-xl">☁️</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 text-sm mb-2">
|
||||
Upload file CSV / XLSX
|
||||
</p>
|
||||
|
||||
<p class="text-gray-600 text-sm mb-2">Upload file CSV / XLSX</p>
|
||||
<p class="text-gray-400 text-xs mb-3">klik tombol di bawah</p>
|
||||
|
||||
<form id="uploadForm" action="{{ route('ulasan.upload') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<input type="file" name="file" id="fileInput"
|
||||
accept=".csv,.xlsx,.xls"
|
||||
class="hidden">
|
||||
|
||||
<button type="button"
|
||||
onclick="document.getElementById('fileInput').click()"
|
||||
<input type="file" name="file" id="fileInput" accept=".csv,.xlsx,.xls" class="hidden">
|
||||
<button type="button" onclick="document.getElementById('fileInput').click()"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm shadow">
|
||||
Pilih File
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- <a href="#" class="block mt-3 text-blue-500 text-xs hover:underline">
|
||||
⬇ Download template CSV
|
||||
</a> -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- FORMAT -->
|
||||
<div class="bg-blue-50 rounded-xl p-5">
|
||||
|
||||
<h4 class="font-semibold text-blue-700 mb-3">
|
||||
Format CSV yang Diperlukan
|
||||
</h4>
|
||||
|
||||
<h4 class="font-semibold text-blue-700 mb-3">Format CSV yang Diperlukan</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-700">
|
||||
<li>✔ wisata, rating, ulasan, tanggal</li>
|
||||
<li>✔ Format: YYYY-MM-DD HH:MM:SS</li>
|
||||
<li>✔ Maks 10MB</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SUMMARY -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-blue-100 text-blue-600 p-3 rounded-full">📄</div>
|
||||
<div>
|
||||
|
|
@ -74,7 +62,6 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm sha
|
|||
<p class="font-bold text-lg">{{ $totalUlasan }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-green-100 text-green-600 p-3 rounded-full">📍</div>
|
||||
<div>
|
||||
|
|
@ -82,7 +69,6 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm sha
|
|||
<p class="font-bold text-lg">{{ $totalWisata }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-orange-100 text-orange-600 p-3 rounded-full">📅</div>
|
||||
<div>
|
||||
|
|
@ -92,11 +78,9 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm sha
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ALERT -->
|
||||
|
|
@ -106,154 +90,107 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm sha
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- 🔍 SEARCH + FILTER -->
|
||||
<!-- ============================= -->
|
||||
<form method="GET" action="{{ route('ulasan.index') }}" class="flex flex-col md:flex-row gap-3">
|
||||
<!-- FILTER PERIODE + SEARCH -->
|
||||
<form method="GET" action="{{ route('ulasan.index') }}" class="flex flex-col md:flex-row gap-3 items-center">
|
||||
|
||||
{{-- DROPDOWN PERIODE --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-600 font-medium whitespace-nowrap">Periode:</span>
|
||||
<select name="periode_id" onchange="this.form.submit()"
|
||||
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm">
|
||||
<option value="">Semua Periode</option>
|
||||
@foreach($periodeList as $p)
|
||||
<option value="{{ $p->id }}" {{ request('periode_id') == $p->id ? 'selected' : '' }}>
|
||||
{{ $p->nama }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- SEARCH --}}
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
placeholder="Cari ulasan atau wisata..."
|
||||
class="flex-1 border px-3 py-2 rounded-lg text-sm">
|
||||
|
||||
<!-- <select name="wisata" onchange="this.form.submit()"
|
||||
class="border rounded-lg px-3 py-2 text-sm">
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm">Cari</button>
|
||||
|
||||
<option value="">Semua Destinasi</option>
|
||||
<option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
|
||||
<option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
|
||||
<option value="Teluk Love" {{ request('wisata')=='Teluk Love'?'selected':'' }}>Teluk Love</option>
|
||||
|
||||
</select> -->
|
||||
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm">
|
||||
Cari
|
||||
</button>
|
||||
|
||||
<a href="{{ route('ulasan.index') }}"
|
||||
class="bg-gray-200 px-4 py-2 rounded-lg text-sm">
|
||||
Reset
|
||||
</a>
|
||||
<a href="{{ route('ulasan.index') }}" class="bg-gray-200 px-4 py-2 rounded-lg text-sm">Reset</a>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- TABLE -->
|
||||
<!-- ============================= -->
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-4 py-3 w-[5%]">No</th>
|
||||
<th class="px-4 py-3 w-[20%]">Nama Wisata</th>
|
||||
<th class="px-4 py-3 w-[10%] text-center">Rating</th>
|
||||
<th class="px-4 py-3 w-[45%]">Ulasan</th>
|
||||
<th class="px-4 py-3 w-[20%] text-center">Tanggal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y">
|
||||
|
||||
@forelse($mentah as $i => $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
|
||||
<td class="px-4 py-3">{{ $mentah->firstItem() + $i }}</td>
|
||||
|
||||
<td class="px-4 py-3 font-medium">
|
||||
{{ $item->wisata }}
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-center text-yellow-500">
|
||||
{{ str_repeat('★', round($item->rating)) }}
|
||||
<span class="text-gray-500 ml-1">{{ $item->rating }}</span>
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3">
|
||||
<div class="line-clamp-2 text-gray-600">
|
||||
{{ $item->ulasan }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-center text-gray-500">
|
||||
{{ $item->tanggal }}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-6 text-gray-400">
|
||||
Belum ada data
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-4 py-3 w-[5%]">No</th>
|
||||
<th class="px-4 py-3 w-[20%]">Nama Wisata</th>
|
||||
<th class="px-4 py-3 w-[10%] text-center">Rating</th>
|
||||
<th class="px-4 py-3 w-[45%]">Ulasan</th>
|
||||
<th class="px-4 py-3 w-[20%] text-center">Tanggal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse($mentah as $i => $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">{{ $mentah->firstItem() + $i }}</td>
|
||||
<td class="px-4 py-3 font-medium">{{ $item->wisata }}</td>
|
||||
<td class="px-4 py-3 text-center text-yellow-500">
|
||||
{{ str_repeat('★', round($item->rating)) }}
|
||||
<span class="text-gray-500 ml-1">{{ $item->rating }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="line-clamp-2 text-gray-600">{{ $item->ulasan }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-gray-500">
|
||||
{{ \Carbon\Carbon::parse($item->tanggal)->diffForHumans() }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-6 text-gray-400">Belum ada data</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex flex-col md:flex-row gap-3 justify-between items-center text-sm text-gray-500">
|
||||
<div>
|
||||
Menampilkan {{ $mentah->firstItem() ?? 0 }} - {{ $mentah->lastItem() ?? 0 }}
|
||||
dari {{ $mentah->total() }} data
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
@if ($mentah->onFirstPage())
|
||||
<span class="px-3 py-1 bg-gray-200 rounded-lg">«</span>
|
||||
@else
|
||||
<a href="{{ $mentah->previousPageUrl() }}" class="px-3 py-1 bg-gray-100 rounded-lg">«</a>
|
||||
@endif
|
||||
|
||||
<!-- KIRI (FINAL FIX) -->
|
||||
<div>
|
||||
Menampilkan {{ $mentah->firstItem() ?? 0 }} - {{ $mentah->lastItem() ?? 0 }}
|
||||
dari {{ $mentah->total() }} data
|
||||
</div>
|
||||
@for ($i = 1; $i <= $mentah->lastPage(); $i++)
|
||||
@if ($i == $mentah->currentPage())
|
||||
<span class="px-3 py-1 bg-blue-600 text-white rounded-lg">{{ $i }}</span>
|
||||
@elseif ($i <= 3 || $i > $mentah->lastPage() - 2 || abs($i - $mentah->currentPage()) <= 1)
|
||||
<a href="{{ $mentah->url($i) }}" class="px-3 py-1 bg-gray-100 rounded-lg">{{ $i }}</a>
|
||||
@elseif ($i == 4 || $i == $mentah->lastPage() - 3)
|
||||
<span class="px-2">...</span>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
<!-- KANAN (PAGINATION) -->
|
||||
<div class="flex items-center gap-1">
|
||||
|
||||
@if ($mentah->onFirstPage())
|
||||
<span class="px-3 py-1 bg-gray-200 rounded-lg">«</span>
|
||||
@else
|
||||
<a href="{{ $mentah->previousPageUrl() }}"
|
||||
class="px-3 py-1 bg-gray-100 rounded-lg">«</a>
|
||||
@endif
|
||||
|
||||
@for ($i = 1; $i <= $mentah->lastPage(); $i++)
|
||||
@if ($i == $mentah->currentPage())
|
||||
<span class="px-3 py-1 bg-blue-600 text-white rounded-lg">{{ $i }}</span>
|
||||
@elseif ($i <= 3 || $i > $mentah->lastPage() - 2 || abs($i - $mentah->currentPage()) <= 1)
|
||||
<a href="{{ $mentah->url($i) }}"
|
||||
class="px-3 py-1 bg-gray-100 rounded-lg">{{ $i }}</a>
|
||||
@elseif ($i == 4 || $i == $mentah->lastPage() - 3)
|
||||
<span class="px-2">...</span>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
@if ($mentah->hasMorePages())
|
||||
<a href="{{ $mentah->nextPageUrl() }}"
|
||||
class="px-3 py-1 bg-gray-100 rounded-lg">»</a>
|
||||
@else
|
||||
<span class="px-3 py-1 bg-gray-200 rounded-lg">»</span>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@if ($mentah->hasMorePages())
|
||||
<a href="{{ $mentah->nextPageUrl() }}" class="px-3 py-1 bg-gray-100 rounded-lg">»</a>
|
||||
@else
|
||||
<span class="px-3 py-1 bg-gray-200 rounded-lg">»</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- BUTTON ANALISIS -->
|
||||
<div class="flex justify-end">
|
||||
<form action="{{ route('ulasan.analisis') }}" method="POST">
|
||||
@csrf
|
||||
<button class="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg">
|
||||
Proses Analisis
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- AUTO SUBMIT UPLOAD -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const input = document.getElementById("fileInput");
|
||||
const form = document.getElementById("uploadForm");
|
||||
|
||||
input.addEventListener("change", function () {
|
||||
form.submit();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,42 +7,61 @@
|
|||
use App\Http\Controllers\AnalisisController;
|
||||
use App\Http\Controllers\RekomendasiController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ROUTE AWAL
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// hanya SATU route '/'
|
||||
Route::get('/', function () {
|
||||
return redirect('/login');
|
||||
});
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ROUTE SETELAH LOGIN
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Dashboard SENTARA (harus login)
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('dashboard');
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
|
||||
// Route::get('/dashboard', function () {
|
||||
// return view('dashboard');
|
||||
// })->middleware(['auth', 'verified'])->name('dashboard');
|
||||
// ✅ DASHBOARD
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])
|
||||
->name('dashboard');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
// ✅ PROFILE (ini yang kamu tanya)
|
||||
Route::get('/profile', function () {
|
||||
return view('profile.index');
|
||||
})->name('profile');
|
||||
|
||||
// kalau mau aktifkan update:
|
||||
// Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
// Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
Route::get('/ulasan', [UlasanController::class, 'index'])->name('ulasan.index');
|
||||
Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])->name('ulasan.ambil');
|
||||
Route::post('/ulasan/analisis', [UlasanController::class, 'analisisData'])->name('ulasan.analisis');
|
||||
Route::post('/ulasan/upload', [UlasanController::class, 'upload'])->name('ulasan.upload');
|
||||
Route::get('/hasil-analisis', [AnalisisController::class, 'index'])
|
||||
->name('analisis.index');
|
||||
Route::get('/rekomendasi', [RekomendasiController::class, 'index'])
|
||||
->name('rekomendasi.index');;
|
||||
Route::get('/', function () {
|
||||
return redirect()->route('login');
|
||||
|
||||
});
|
||||
|
||||
// ✅ DATA ULASAN
|
||||
Route::get('/ulasan', [UlasanController::class, 'index'])
|
||||
->name('ulasan.index');
|
||||
|
||||
// Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])
|
||||
// ->name('ulasan.ambil');
|
||||
|
||||
Route::post('/ulasan/upload', [UlasanController::class, 'upload'])
|
||||
->name('ulasan.upload');
|
||||
|
||||
// ✅ PROSES ANALISIS
|
||||
Route::post('/proses-analisis', [UlasanController::class, 'analisisData'])
|
||||
->name('proses.analisis');
|
||||
|
||||
// ✅ HASIL ANALISIS
|
||||
Route::get('/analisis', [AnalisisController::class, 'index'])
|
||||
->name('analisis.index');
|
||||
|
||||
// ✅ REKOMENDASI
|
||||
Route::get('/rekomendasi', function() {
|
||||
return redirect('/dashboard?tab=rekomendasi');
|
||||
})->name('rekomendasi.index');
|
||||
|
||||
});
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
require __DIR__.'/auth.php';
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
import re
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pymysql
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
|
|
@ -10,33 +16,45 @@ from sklearn.metrics import accuracy_score, classification_report, confusion_mat
|
|||
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
|
||||
from Sastrawi.StopWordRemover.StopWordRemoverFactory import StopWordRemoverFactory
|
||||
|
||||
|
||||
# ===============================
|
||||
# 🔌 KONEKSI DB
|
||||
# KONEKSI DB
|
||||
# ===============================
|
||||
conn = pymysql.connect(
|
||||
engine = create_engine("mysql+pymysql://root:@localhost/sentara")
|
||||
|
||||
raw_conn = pymysql.connect(
|
||||
host='localhost',
|
||||
user='root',
|
||||
password='',
|
||||
database='sentara',
|
||||
cursorclass=pymysql.cursors.DictCursor
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
cursor = raw_conn.cursor()
|
||||
|
||||
# ===============================
|
||||
# 1. AMBIL DATA
|
||||
# 1. AMBIL PERIODE AKTIF (TERBARU)
|
||||
# ===============================
|
||||
df = pd.read_sql("SELECT id, wisata, ulasan FROM ulasan", conn)
|
||||
cursor.execute("SELECT id, nama FROM periode_analisis ORDER BY id DESC LIMIT 1")
|
||||
periode = cursor.fetchone()
|
||||
periode_id = periode['id']
|
||||
periode_nama = periode['nama']
|
||||
print(f"[INFO] Analisis periode: {periode_nama} (id={periode_id})")
|
||||
|
||||
# ===============================
|
||||
# 2. CLEANING
|
||||
# 2. AMBIL DATA PERIODE INI SAJA
|
||||
# ===============================
|
||||
df = pd.read_sql(f"SELECT id, wisata, ulasan FROM ulasan WHERE periode_id = {periode_id}", engine)
|
||||
|
||||
# ===============================
|
||||
# 3. CLEANING
|
||||
# ===============================
|
||||
df = df.dropna(subset=["ulasan"])
|
||||
df["ulasan"] = df["ulasan"].astype(str)
|
||||
df = df[df["ulasan"].str.strip() != ""]
|
||||
df = df[df["ulasan"].str.strip() != "0"]
|
||||
|
||||
# ===============================
|
||||
# 3. PREPROCESSING
|
||||
# 4. PREPROCESSING
|
||||
# ===============================
|
||||
stemmer = StemmerFactory().create_stemmer()
|
||||
stop_factory = StopWordRemoverFactory()
|
||||
|
|
@ -46,37 +64,57 @@ def clean_text(text):
|
|||
text = text.lower()
|
||||
text = re.sub(r"http\S+", " ", text)
|
||||
text = re.sub(r"[^a-zA-Z\s]", " ", text)
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
|
||||
words = [w for w in text.split() if w not in stopwords]
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
words = [w for w in text.split() if w not in stopwords and len(w) > 2]
|
||||
return stemmer.stem(" ".join(words))
|
||||
|
||||
df["clean"] = df["ulasan"].apply(clean_text)
|
||||
df = df[df["clean"].str.strip() != ""]
|
||||
|
||||
# ===============================
|
||||
# 4. LABEL SEMENTARA (dummy)
|
||||
# 5. LABEL (RULE BASED)
|
||||
# ===============================
|
||||
positif_words = {
|
||||
"bagus", "indah", "mantap", "keren", "cantik", "menarik", "nyaman",
|
||||
"bersih", "recommended", "suka", "senang", "puas", "murah", "asyik",
|
||||
"ramah", "worth", "spektakuler", "memukau", "sejuk", "baguss", "kece",
|
||||
"amazing", "beautiful", "good", "nice", "best", "great", "perfect",
|
||||
"recommend", "memuaskan", "menyenangkan", "view"
|
||||
}
|
||||
negatif_words = {
|
||||
"tidak", "buruk", "mahal", "jelek", "kotor", "kecewa", "rusak",
|
||||
"sempit", "panas", "bau", "berbahaya", "sepi", "bosan", "mengecewakan",
|
||||
"payah", "parah", "jorok", "macet", "antri", "penuh", "sampah",
|
||||
"sayang", "kurang", "susah", "sulit", "jauh", "capek", "lelah"
|
||||
}
|
||||
|
||||
def label_rule(text):
|
||||
if "tidak" in text or "buruk" in text:
|
||||
return "negatif"
|
||||
elif "bagus" in text or "indah" in text:
|
||||
words = set(text.split())
|
||||
skor_pos = len(words & positif_words)
|
||||
skor_neg = len(words & negatif_words)
|
||||
if skor_pos > skor_neg:
|
||||
return "positif"
|
||||
elif skor_neg > skor_pos:
|
||||
return "negatif"
|
||||
else:
|
||||
return "netral"
|
||||
|
||||
df["label"] = df["clean"].apply(label_rule)
|
||||
|
||||
# ===============================
|
||||
# 5. TF-IDF + MODEL
|
||||
# 6. TF-IDF + MODEL
|
||||
# ===============================
|
||||
X = df["clean"]
|
||||
y = df["label"]
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
if len(df) > 5:
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
else:
|
||||
X_train, X_test, y_train, y_test = X, X, y, y
|
||||
|
||||
vectorizer = TfidfVectorizer()
|
||||
vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1,2))
|
||||
X_train_vec = vectorizer.fit_transform(X_train)
|
||||
X_test_vec = vectorizer.transform(X_test)
|
||||
|
||||
|
|
@ -84,59 +122,65 @@ model = ComplementNB()
|
|||
model.fit(X_train_vec, y_train)
|
||||
|
||||
# ===============================
|
||||
# 6. EVALUASI
|
||||
# 7. EVALUASI
|
||||
# ===============================
|
||||
y_pred = model.predict(X_test_vec)
|
||||
|
||||
acc = accuracy_score(y_test, y_pred)
|
||||
report = classification_report(y_test, y_pred, output_dict=True)
|
||||
report = classification_report(y_test, y_pred, output_dict=True, zero_division=0)
|
||||
cm = confusion_matrix(y_test, y_pred, labels=["negatif","netral","positif"])
|
||||
|
||||
# ===============================
|
||||
# 7. PREDIKSI SEMUA DATA
|
||||
# 8. PREDIKSI SEMUA DATA
|
||||
# ===============================
|
||||
X_all = vectorizer.transform(df["clean"])
|
||||
df["prediksi"] = model.predict(X_all)
|
||||
|
||||
# ===============================
|
||||
# 8. SIMPAN KE DB
|
||||
# 9. SIMPAN KE DB
|
||||
# ===============================
|
||||
df = df.fillna("")
|
||||
|
||||
# kosongkan tabel
|
||||
cursor.execute("DELETE FROM hasil_analisis")
|
||||
cursor.execute("DELETE FROM evaluasi_model")
|
||||
# Hapus data periode ini saja (bukan semua)
|
||||
cursor.execute("DELETE FROM hasil_analisis WHERE periode_id = %s", (periode_id,))
|
||||
cursor.execute("DELETE FROM evaluasi_model WHERE periode_id = %s", (periode_id,))
|
||||
|
||||
insert_query = """
|
||||
INSERT INTO hasil_analisis
|
||||
(wisata, ulasan_asli, ulasan_bersih, hasil_preprocessing, sentimen, probabilitas, periode_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
# simpan hasil per ulasan
|
||||
for _, row in df.iterrows():
|
||||
cursor.execute("""
|
||||
INSERT INTO hasil_analisis
|
||||
(wisata, ulasan_terolah, hasil_preprocessing, sentimen, probabilitas, created_at)
|
||||
VALUES (%s,%s,%s,%s,%s,NOW())
|
||||
""", (
|
||||
row["wisata"],
|
||||
row["ulasan"],
|
||||
row["clean"],
|
||||
row["prediksi"],
|
||||
0.9
|
||||
cursor.execute(insert_query, (
|
||||
str(row["wisata"]),
|
||||
str(row["ulasan"]),
|
||||
str(row["clean"]),
|
||||
str(row["clean"]),
|
||||
str(row["prediksi"]),
|
||||
float(0.9),
|
||||
periode_id
|
||||
))
|
||||
|
||||
# simpan evaluasi
|
||||
print(f"[OK] {len(df)} ulasan berhasil disimpan untuk periode {periode_nama}")
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO evaluasi_model
|
||||
(precision, recall, f1_score, accuracy, tp, tn, fp, fn, created_at)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
||||
(`precision`, `recall`, f1_score, accuracy, tp, tn, fp, fn, periode_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
report["weighted avg"]["precision"],
|
||||
report["weighted avg"]["recall"],
|
||||
report["weighted avg"]["f1-score"],
|
||||
acc,
|
||||
int(cm[2][2]),
|
||||
int(cm[0][0]),
|
||||
int(cm[0][2]),
|
||||
int(cm[2][0])
|
||||
float(report["weighted avg"]["precision"]),
|
||||
float(report["weighted avg"]["recall"]),
|
||||
float(report["weighted avg"]["f1-score"]),
|
||||
float(acc),
|
||||
int(cm[2][2]) if cm.shape == (3,3) else 0,
|
||||
int(cm[0][0]) if cm.shape == (3,3) else 0,
|
||||
int(cm[0][2]) if cm.shape == (3,3) else 0,
|
||||
int(cm[2][0]) if cm.shape == (3,3) else 0,
|
||||
periode_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
raw_conn.commit()
|
||||
raw_conn.close()
|
||||
|
||||
print("Analisis selesai ✔")
|
||||
print("Analisis selesai [OK]")
|
||||
Loading…
Reference in New Issue