nambah aja
This commit is contained in:
parent
227aad6557
commit
8e78dce219
|
|
@ -4,30 +4,29 @@
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\HasilAnalisis;
|
use App\Models\HasilAnalisis;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class AnalisisController extends Controller
|
class AnalisisController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
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')
|
$wisataList = HasilAnalisis::select('wisata')
|
||||||
->distinct()
|
->distinct()
|
||||||
->pluck('wisata');
|
->pluck('wisata');
|
||||||
|
|
||||||
// Query utama tabel
|
// Query hasil analisis dengan filter
|
||||||
$query = HasilAnalisis::query();
|
$query = HasilAnalisis::query();
|
||||||
|
|
||||||
// Filter kalau user pilih destinasi tertentu
|
|
||||||
if ($request->filled('wisata')) {
|
if ($request->filled('wisata')) {
|
||||||
$query->where('wisata', $request->wisata);
|
$query->where('wisata', $request->wisata);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data hasil analisis (pakai paginate biar rapi)
|
$hasil = $query->latest()->paginate(10);
|
||||||
$hasil = $query->latest()->paginate(5);
|
|
||||||
|
|
||||||
return view('analisis.index', compact(
|
return view('analisis.index', compact('hasil', 'wisataList', 'evaluasi'));
|
||||||
'hasil',
|
|
||||||
'wisataList'
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,138 +7,302 @@
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
private array $issueRules = [
|
||||||
{
|
'Kebersihan' => [
|
||||||
$wisata = $request->wisata;
|
'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')
|
$evaluasi = DB::table('evaluasi_model')
|
||||||
->latest()
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
->first();
|
->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();
|
|
||||||
|
|
||||||
// =============================
|
// ================================================================
|
||||||
// CEK ADA ANALISIS ATAU BELUM
|
// TOTAL ULASAN
|
||||||
// =============================
|
// ================================================================
|
||||||
$hasAnalisis = DB::table('hasil_analisis')->count() > 0;
|
$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)
|
// CEK ADA ANALISIS ATAU BELUM
|
||||||
// =============================
|
// ================================================================
|
||||||
$positif = DB::table('hasil_analisis')
|
$hasAnalisis = DB::table('hasil_analisis')
|
||||||
->where('sentimen', 'Positif')
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
->count() > 0;
|
||||||
$q->where('wisata', $wisata);
|
|
||||||
})
|
|
||||||
->count();
|
|
||||||
|
|
||||||
$negatif = DB::table('hasil_analisis')
|
// ================================================================
|
||||||
->where('sentimen', 'Negatif')
|
// HITUNG SENTIMEN
|
||||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
// ================================================================
|
||||||
$q->where('wisata', $wisata);
|
$positif = DB::table('hasil_analisis')
|
||||||
})
|
->where('sentimen', 'Positif')
|
||||||
->count();
|
->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')
|
$negatif = DB::table('hasil_analisis')
|
||||||
->where('sentimen', 'Netral')
|
->where('sentimen', 'Negatif')
|
||||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
$q->where('wisata', $wisata);
|
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||||
})
|
->count();
|
||||||
->count();
|
|
||||||
|
|
||||||
// =============================
|
$netral = DB::table('hasil_analisis')
|
||||||
// PERSENTASE SENTIMEN
|
->where('sentimen', 'Netral')
|
||||||
// =============================
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
$totalSentimen = $positif + $negatif + $netral;
|
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||||
|
->count();
|
||||||
|
|
||||||
$stats = [
|
$totalSentimen = $positif + $negatif + $netral;
|
||||||
'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,
|
|
||||||
];
|
|
||||||
|
|
||||||
// =============================
|
$stats = [
|
||||||
// GRAFIK PIE (Distribusi Sentimen)
|
'total' => $totalUlasan,
|
||||||
// =============================
|
'positif_persen' => $totalSentimen > 0 ? round(($positif / $totalSentimen) * 100) : 0,
|
||||||
$chartSentimen = $hasAnalisis
|
'negatif_persen' => $totalSentimen > 0 ? round(($negatif / $totalSentimen) * 100) : 0,
|
||||||
? DB::table('hasil_analisis')
|
'netral_persen' => $totalSentimen > 0 ? round(($netral / $totalSentimen) * 100) : 0,
|
||||||
->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();
|
|
||||||
|
|
||||||
// =============================
|
// ================================================================
|
||||||
// GRAFIK BAR (Sentimen per Destinasi)
|
// CHART DATA
|
||||||
// =============================
|
// ================================================================
|
||||||
$chartDestinasi = $hasAnalisis
|
$chartSentimen = $hasAnalisis
|
||||||
? DB::table('hasil_analisis')
|
? DB::table('hasil_analisis')
|
||||||
->select(
|
->select('sentimen', DB::raw('count(*) as total'))
|
||||||
'wisata',
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
DB::raw("SUM(CASE WHEN sentimen = 'Positif' THEN 1 ELSE 0 END) as positif"),
|
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||||
DB::raw("SUM(CASE WHEN sentimen = 'Negatif' THEN 1 ELSE 0 END) as negatif"),
|
->groupBy('sentimen')
|
||||||
DB::raw("SUM(CASE WHEN sentimen = 'Netral' THEN 1 ELSE 0 END) as netral")
|
->orderByRaw("FIELD(sentimen, 'Positif', 'Negatif', 'Netral')")
|
||||||
)
|
->get()
|
||||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
: collect();
|
||||||
$q->where('wisata', $wisata);
|
|
||||||
})
|
|
||||||
->groupBy('wisata')
|
|
||||||
|
|
||||||
->get()
|
|
||||||
: collect();
|
|
||||||
|
|
||||||
// =============================
|
$chartDestinasi = $hasAnalisis
|
||||||
// TOTAL DATA PER DESTINASI (DARI ULASAN)
|
? DB::table('hasil_analisis')
|
||||||
// =============================
|
->select(
|
||||||
$totalPerWisata = DB::table('ulasan')
|
'wisata',
|
||||||
->select('wisata', DB::raw('count(*) as total'))
|
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')
|
->groupBy('wisata')
|
||||||
->get();
|
->get()
|
||||||
|
: collect();
|
||||||
|
|
||||||
// =============================
|
// ================================================================
|
||||||
// WORD CLOUD (DARI PREPROCESSING)
|
// TOTAL PER DESTINASI + WORD CLOUD + LAST UPDATE
|
||||||
// =============================
|
// ================================================================
|
||||||
$texts = DB::table('preprocessing_data')->pluck('stemming')->toArray();
|
$totalPerWisata = DB::table('ulasan')
|
||||||
$allText = count($texts) > 0 ? implode(' ', $texts) : '';
|
->select('wisata', DB::raw('count(*) as total'))
|
||||||
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
|
->groupBy('wisata')
|
||||||
|
->get();
|
||||||
|
|
||||||
// =============================
|
$texts = DB::table('hasil_analisis')
|
||||||
// LAST UPDATE
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
// =============================
|
->pluck('ulasan_bersih')
|
||||||
$lastUpdate = DB::table('hasil_analisis')
|
->toArray();
|
||||||
->latest('created_at')
|
$allText = count($texts) > 0 ? implode(' ', $texts) : '';
|
||||||
->value('created_at');
|
|
||||||
|
|
||||||
$destinasiList = DB::table('ulasan')
|
$lastUpdate = DB::table('hasil_analisis')
|
||||||
->select('wisata')
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
->distinct()
|
->latest('created_at')->value('created_at');
|
||||||
->pluck('wisata');
|
|
||||||
|
|
||||||
$hasil = DB::table('hasil_analisis')
|
$destinasiList = DB::table('ulasan')
|
||||||
->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) {
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
$q->where('wisata', $wisata);
|
->select('wisata')->distinct()->pluck('wisata');
|
||||||
})
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
return view('dashboard', compact(
|
$hasil = DB::table('hasil_analisis')
|
||||||
'stats',
|
->when($periodeId, fn($q) => $q->where('periode_id', $periodeId))
|
||||||
'chartSentimen',
|
->when($wisata && $wisata != 'Semua Destinasi', fn($q) => $q->where('wisata', $wisata))
|
||||||
'chartDestinasi',
|
->paginate(10);
|
||||||
'totalPerWisata',
|
|
||||||
'allText',
|
// ================================================================
|
||||||
'lastUpdate',
|
// REKOMENDASI
|
||||||
'hasAnalisis',
|
// ================================================================
|
||||||
'destinasiList', // WAJIB
|
$ulasanNegatif = DB::table('hasil_analisis')
|
||||||
'hasil',
|
->where('sentimen', 'Negatif')
|
||||||
'evaluasi' // WAJIB
|
->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
|
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)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
// dropdown destinasi unik
|
// --- 1. Dropdown destinasi ---
|
||||||
$destinasiList = HasilAnalisis::select('wisata')
|
$destinasiList = DB::table('hasil_analisis')
|
||||||
|
->select('wisata')
|
||||||
->distinct()
|
->distinct()
|
||||||
->pluck('wisata');
|
->pluck('wisata');
|
||||||
|
|
||||||
// query utama
|
// --- 2. Query dengan filter destinasi ---
|
||||||
$query = HasilAnalisis::query();
|
$queryAll = HasilAnalisis::query();
|
||||||
|
$queryFilter = HasilAnalisis::query();
|
||||||
|
|
||||||
// filter destinasi jika dipilih
|
|
||||||
if ($request->filled('destinasi')) {
|
if ($request->filled('destinasi')) {
|
||||||
$query->where('wisata', $request->destinasi);
|
$queryFilter->where('wisata', $request->destinasi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ambil ulasan negatif
|
// --- 3. Hitung total & sentimen ---
|
||||||
$negatif = $query->where('sentimen', 'negatif')
|
$totalUlasan = $queryFilter->count();
|
||||||
->pluck('ulasan_terolah')
|
$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();
|
->toArray();
|
||||||
|
|
||||||
// gabungkan semua ulasan jadi 1 string
|
$text = strtolower(implode(' ', $ulasanNegatif));
|
||||||
$text = strtolower(implode(" ", $negatif));
|
$words = array_filter(preg_split('/\s+/', $text));
|
||||||
|
|
||||||
// token kata sederhana
|
// --- 5. Rule-based: hitung skor tiap kategori isu ---
|
||||||
$words = array_filter(explode(" ", $text));
|
$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);
|
$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);
|
arsort($freq);
|
||||||
|
$kataDominan = array_slice($freq, 0, 10, true);
|
||||||
|
|
||||||
// ambil top 5 keyword
|
// --- 9. Prioritas rekomendasi (top 3 isu) ---
|
||||||
$topKeywords = array_slice(array_keys($freq), 0, 5);
|
$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
|
// --- 10. Saran perbaikan (top 3) ---
|
||||||
$isuUtama = $topKeywords[0] ?? "Belum ada isu";
|
$saranPerbaikan = [];
|
||||||
$kataDominan = implode(", ", $topKeywords);
|
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
|
// --- 11. Filter destinasi yang sedang aktif ---
|
||||||
$saran = "Perlu peningkatan layanan terkait: " . $isuUtama;
|
$destinasiAktif = $request->input('destinasi', 'Semua Destinasi');
|
||||||
|
|
||||||
$destinasiList = DB::table('hasil_analisis')
|
return view('rekomendasi.index', compact(
|
||||||
->select('wisata')
|
'destinasiList',
|
||||||
->distinct()
|
'destinasiAktif',
|
||||||
->pluck('wisata');
|
'totalUlasan',
|
||||||
|
'totalNegatif',
|
||||||
|
'totalPositif',
|
||||||
return view('dashboard', compact(
|
'totalNetral',
|
||||||
'stats',
|
'persenNegatif',
|
||||||
'chartSentimen',
|
'tingkatKepuasan',
|
||||||
'chartDestinasi',
|
'labelKepuasan',
|
||||||
'totalPerWisata',
|
'isuUtama',
|
||||||
'allText',
|
'isuDominan',
|
||||||
'lastUpdate',
|
'isuDominanPersen',
|
||||||
'hasAnalisis',
|
'kataDominan',
|
||||||
'wisataList',
|
'saranPerbaikan',
|
||||||
'hasil',
|
'prioritas',
|
||||||
'destinasiList' // ✅ TAMBAH INI
|
));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ class UlasanController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
|
$periodeList = DB::table('periode_analisis')->orderBy('id', 'desc')->get();
|
||||||
$query = Ulasan::query();
|
$query = Ulasan::query();
|
||||||
|
|
||||||
// 🔍 SEARCH (ulasan + wisata)
|
// 🔍 SEARCH (ulasan + wisata)
|
||||||
|
|
@ -29,87 +30,116 @@ public function index(Request $request)
|
||||||
$query->where('wisata', 'LIKE', '%' . $request->wisata . '%');
|
$query->where('wisata', 'LIKE', '%' . $request->wisata . '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
$mentah = $query->latest()->paginate(20)->withQueryString();
|
// ← filter periode
|
||||||
|
if ($request->filled('periode_id')) {
|
||||||
// OPTIONAL
|
$query->where('periode_id', $request->periode_id);
|
||||||
$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(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('ulasan.index')
|
$mentah = $query->orderBy('tanggal', 'desc')->paginate(20)->withQueryString();
|
||||||
->with('success', 'Upload berhasil dan data masuk!');
|
|
||||||
|
// 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()
|
public function upload(Request $request)
|
||||||
{
|
{
|
||||||
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
|
$request->validate([
|
||||||
$scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"';
|
'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')
|
$periode = DB::table('periode_analisis')
|
||||||
->with('success', 'Analisis berhasil dijalankan');
|
->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;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class HasilAnalisis extends Model
|
class HasilAnalisis extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $table = 'hasil_analisis';
|
protected $table = 'hasil_analisis';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'wisata',
|
'wisata',
|
||||||
'ulasan_terolah',
|
'ulasan_asli', // ← fix dari ulasan_terolah
|
||||||
|
'ulasan_bersih', // ← tambah ini
|
||||||
|
'hasil_preprocessing',
|
||||||
'sentimen',
|
'sentimen',
|
||||||
'probabilitas'
|
'probabilitas'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ public function register(): void
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Paginator::useTailwind();
|
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.
|
Berikut adalah hasil evaluasi model dan detail analisis sentimen terhadap ulasan wisata.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="text-sm text-gray-500">
|
|
||||||
{{ now()->format('d F Y - H:i') }}
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ================= METRICS ================= --}}
|
{{-- ================= METRICS ================= --}}
|
||||||
|
|
@ -19,28 +16,28 @@
|
||||||
<div class="bg-white p-5 rounded-xl shadow">
|
<div class="bg-white p-5 rounded-xl shadow">
|
||||||
<p class="text-gray-500 text-sm">Precision</p>
|
<p class="text-gray-500 text-sm">Precision</p>
|
||||||
<h3 class="text-2xl font-bold text-blue-600">
|
<h3 class="text-2xl font-bold text-blue-600">
|
||||||
{{ $evaluasi->precision ?? 0 }}
|
{{ number_format($evaluasi->precision ?? 0, 2) }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white p-5 rounded-xl shadow">
|
<div class="bg-white p-5 rounded-xl shadow">
|
||||||
<p class="text-gray-500 text-sm">Recall</p>
|
<p class="text-gray-500 text-sm">Recall</p>
|
||||||
<h3 class="text-2xl font-bold text-green-600">
|
<h3 class="text-2xl font-bold text-green-600">
|
||||||
{{ $evaluasi->recall ?? 0 }}
|
{{ number_format($evaluasi->recall ?? 0, 2) }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white p-5 rounded-xl shadow">
|
<div class="bg-white p-5 rounded-xl shadow">
|
||||||
<p class="text-gray-500 text-sm">F1 Score</p>
|
<p class="text-gray-500 text-sm">F1 Score</p>
|
||||||
<h3 class="text-2xl font-bold text-purple-600">
|
<h3 class="text-2xl font-bold text-purple-600">
|
||||||
{{ $evaluasi->f1_score ?? 0 }}
|
{{ number_format($evaluasi->f1_score ?? 0, 2) }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white p-5 rounded-xl shadow">
|
<div class="bg-white p-5 rounded-xl shadow">
|
||||||
<p class="text-gray-500 text-sm">Akurasi</p>
|
<p class="text-gray-500 text-sm">Akurasi</p>
|
||||||
<h3 class="text-2xl font-bold text-indigo-600">
|
<h3 class="text-2xl font-bold text-indigo-600">
|
||||||
{{ $evaluasi->accuracy ?? 0 }}
|
{{ number_format($evaluasi->accuracy ?? 0, 2) }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -66,8 +63,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class="bg-gray-50">Positif</th>
|
<th class="bg-gray-50">Positif</th>
|
||||||
<td class="bg-green-100">{{ $evaluasi->tp ?? 0 }}</td>
|
<td class="bg-green-100">{{ $evaluasi->tp ?? 0 }}</td>
|
||||||
<td class="bg-red-100">{{ $evaluasi->fn ?? 0 }}</td>
|
<td class="bg-red-100">{{ $evaluasi->fp ?? 0 }}</td>
|
||||||
<td class="bg-yellow-100">0</td>
|
<td class="bg-yellow-100">{{ $evaluasi->fn ?? 0 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="bg-gray-50">Negatif</th>
|
<th class="bg-gray-50">Negatif</th>
|
||||||
|
|
@ -96,29 +93,28 @@
|
||||||
<th>Precision</th>
|
<th>Precision</th>
|
||||||
<th>Recall</th>
|
<th>Recall</th>
|
||||||
<th>F1</th>
|
<th>F1</th>
|
||||||
<th>Support</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-green-600 font-semibold">Positif</td>
|
<td class="text-green-600 font-semibold">Positif</td>
|
||||||
<td>{{ $evaluasi->precision ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->precision ?? 0, 2) }}</td>
|
||||||
<td>{{ $evaluasi->recall ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->recall ?? 0, 2) }}</td>
|
||||||
<td>{{ $evaluasi->f1_score ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->f1_score ?? 0, 2) }}</td>
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-red-600 font-semibold">Negatif</td>
|
<td class="text-red-600 font-semibold">Negatif</td>
|
||||||
<td>{{ $evaluasi->precision ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->precision ?? 0, 2) }}</td>
|
||||||
<td>{{ $evaluasi->recall ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->recall ?? 0, 2) }}</td>
|
||||||
<td>{{ $evaluasi->f1_score ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->f1_score ?? 0, 2) }}</td>
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-yellow-600 font-semibold">Netral</td>
|
<td class="text-yellow-600 font-semibold">Netral</td>
|
||||||
<td>{{ $evaluasi->precision ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->precision ?? 0, 2) }}</td>
|
||||||
<td>{{ $evaluasi->recall ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->recall ?? 0, 2) }}</td>
|
||||||
<td>{{ $evaluasi->f1_score ?? 0 }}</td>
|
<td>{{ number_format($evaluasi->f1_score ?? 0, 2) }}</td>
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -128,25 +124,23 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ================= FILTER ================= --}}
|
{{-- ================= 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">
|
<select name="wisata" class="border rounded px-3 py-2">
|
||||||
<option value="">Semua Destinasi</option>
|
<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)
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
|
||||||
<option value="{{ $d }}" {{ request('wisata') == $d ? 'selected' : '' }}>
|
Filter
|
||||||
{{ $d }}
|
</button>
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
|
</form>
|
||||||
Filter
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{-- ================= TABLE ================= --}}
|
{{-- ================= TABLE ================= --}}
|
||||||
<div class="bg-white rounded-xl shadow p-4">
|
<div class="bg-white rounded-xl shadow p-4">
|
||||||
|
|
@ -158,102 +152,100 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>No</th>
|
<th>No</th>
|
||||||
<th>Wisata</th>
|
<th>Wisata</th>
|
||||||
<th>Ulasan</th>
|
<th>Ulasan Asli</th>
|
||||||
|
<th>Ulasan Bersih</th>
|
||||||
<th>Sentimen</th>
|
<th>Sentimen</th>
|
||||||
<th>Probabilitas</th>
|
<th>Probabilitas</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse($hasil as $index => $item)
|
@forelse($hasil as $item)
|
||||||
<tr class="border-b">
|
<tr class="border-b">
|
||||||
<td>{{ $loop->iteration }}</td>
|
<td>{{ ($hasil->currentPage() - 1) * $hasil->perPage() + $loop->iteration }}</td>
|
||||||
<td>{{ $item->wisata }}</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>
|
<td>
|
||||||
<span class="
|
<span class="px-2 py-1 rounded text-xs
|
||||||
px-2 py-1 rounded text-xs
|
{{ strtolower($item->sentimen) == 'positif' ? 'bg-green-100 text-green-600' : '' }}
|
||||||
{{ $item->sentimen == 'Positif' ? 'bg-green-100 text-green-600' : '' }}
|
{{ strtolower($item->sentimen) == 'negatif' ? 'bg-red-100 text-red-600' : '' }}
|
||||||
{{ $item->sentimen == 'Negatif' ? 'bg-red-100 text-red-600' : '' }}
|
{{ strtolower($item->sentimen) == 'netral' ? 'bg-yellow-100 text-yellow-600' : '' }}
|
||||||
{{ $item->sentimen == 'Netral' ? 'bg-yellow-100 text-yellow-600' : '' }}
|
|
||||||
">
|
">
|
||||||
{{ $item->sentimen }}
|
{{ ucfirst($item->sentimen) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ number_format($item->probabilitas ?? 0, 2) }}</td>
|
||||||
<td>{{ $item->probabilitas ?? '-' }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-center py-4 text-gray-500">
|
<td colspan="6" class="text-center py-4 text-gray-500">Tidak ada data</td>
|
||||||
Tidak ada data
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{{-- PAGINATION --}}
|
{{-- ================= PAGINATION ================= --}}
|
||||||
<div class="mt-6 flex items-center justify-between text-sm text-gray-500">
|
@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 class="mt-4 flex items-center justify-between text-sm text-gray-600">
|
||||||
<div>
|
|
||||||
Menampilkan {{ $hasil->firstItem() }} - {{ $hasil->lastItem() }}
|
|
||||||
dari {{ $hasil->total() }} data
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- PAGINATION --}}
|
{{-- Info --}}
|
||||||
{{-- PAGINATION --}}
|
<p>Menampilkan {{ $hasil->firstItem() }} - {{ $hasil->lastItem() }} dari {{ $hasil->total() }} data</p>
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
|
|
||||||
{{-- PREV --}}
|
{{-- Tombol --}}
|
||||||
@if ($hasil->onFirstPage())
|
<div class="flex items-center gap-1">
|
||||||
<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
|
|
||||||
|
|
||||||
{{-- 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
|
{{-- Halaman 1 (jika tidak ada di window) --}}
|
||||||
$query = request()->query();
|
@if(!$window->contains(1))
|
||||||
$query['page'] = $page;
|
<a href="{{ $hasil->url(1) }}&{{ $query }}" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">1</a>
|
||||||
$customUrl = url()->current() . '?' . http_build_query($query);
|
@if($window->min() > 2)
|
||||||
@endphp
|
<span class="px-2 py-1 text-gray-400">...</span>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
@if ($page == $hasil->currentPage())
|
{{-- Window --}}
|
||||||
<span class="px-3 py-1 rounded bg-blue-600 text-white">
|
@foreach($window as $page)
|
||||||
{{ $page }}
|
@if($page == $current)
|
||||||
</span>
|
<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">
|
@if($hasil->hasMorePages())
|
||||||
{{ $page }}
|
<a href="{{ $hasil->nextPageUrl() }}&{{ $query }}" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">»</a>
|
||||||
</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)
|
</div>
|
||||||
<span class="px-2">...</span>
|
</div>
|
||||||
@endif
|
@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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -11,264 +11,243 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div x-data="{ tab: 'dashboard' }" class="mb-6">
|
{{-- DROPDOWN PERIODE --}}
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
<!-- TAB -->
|
<div class="flex items-center gap-3">
|
||||||
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
<span class="text-sm text-gray-600 font-medium">Periode Analisis:</span>
|
||||||
|
<form method="GET" action="{{ route('dashboard') }}" id="periode-form">
|
||||||
<button @click="tab='dashboard'"
|
<input type="hidden" name="tab" id="tab-hidden" value="{{ request('tab', 'dashboard') }}">
|
||||||
:class="tab==='dashboard'
|
<input type="hidden" name="wisata" value="{{ request('wisata') }}">
|
||||||
? 'bg-white text-blue-700 shadow'
|
<input type="hidden" name="destinasi" value="{{ request('destinasi') }}">
|
||||||
: 'text-white hover:bg-blue-600'"
|
<select name="periode_id" onchange="document.getElementById('periode-form').submit()"
|
||||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm">
|
||||||
Dashboard Sentara
|
@foreach($periodeList as $p)
|
||||||
</button>
|
<option value="{{ $p->id }}" {{ ($periodeAktif && $periodeAktif->id == $p->id) ? 'selected' : '' }}>
|
||||||
|
{{ $p->nama }}
|
||||||
<button @click="tab='analisis'"
|
</option>
|
||||||
:class="tab==='analisis'
|
@endforeach
|
||||||
? 'bg-white text-blue-700 shadow'
|
</select>
|
||||||
: 'text-white hover:bg-blue-600'"
|
</form>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
@if($periodeAktif)
|
||||||
<div class="bg-white rounded-xl shadow p-5">
|
<span class="text-xs text-gray-400">
|
||||||
<p class="text-gray-500 text-sm">Sentimen Positif</p>
|
Data dianalisis: {{ \Carbon\Carbon::parse($periodeAktif->created_at)->format('d M Y') }}
|
||||||
<h3 class="text-2xl font-bold mt-2 text-green-600">
|
</span>
|
||||||
{{ $stats['positif_persen'] }}%
|
@endif
|
||||||
</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>
|
||||||
|
|
||||||
{{-- Chart Section --}}
|
<div x-data="{ tab: new URLSearchParams(window.location.search).get('tab') || 'dashboard' }" class="mb-6">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
||||||
|
|
||||||
<!-- Pie Chart -->
|
<!-- TAB -->
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
||||||
<h3 class="font-semibold mb-4 text-gray-700">Distribusi Sentimen</h3>
|
<button @click="tab='dashboard'; document.getElementById('tab-hidden').value='dashboard'"
|
||||||
<div style="height:300px;">
|
:class="tab==='dashboard' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||||
<canvas id="pieChart"></canvas>
|
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||||
</div>
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Bar Chart -->
|
<!-- ================= DASHBOARD ================= -->
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
<div x-show="tab === 'dashboard'">
|
||||||
<h3 class="font-semibold mb-4 text-gray-700">Sentimen per Destinasi</h3>
|
|
||||||
<div style="height:300px;">
|
|
||||||
<canvas id="barChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
{{-- Statistik Cards --}}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
{{-- Section Bawah --}}
|
<div class="bg-white rounded-xl shadow p-5">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<p class="text-gray-500 text-sm">Total Ulasan</p>
|
||||||
|
<h3 class="text-2xl font-bold mt-2 text-gray-900">{{ $stats['total'] }}</h3>
|
||||||
<!-- Total Data per Destinasi -->
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
<div class="bg-white rounded-xl shadow p-5">
|
||||||
<h3 class="font-semibold mb-6 text-gray-700">
|
<p class="text-gray-500 text-sm">Sentimen Positif</p>
|
||||||
Total Data per Destinasi
|
<h3 class="text-2xl font-bold mt-2 text-green-600">{{ $stats['positif_persen'] }}%</h3>
|
||||||
</h3>
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow p-5">
|
||||||
<div class="space-y-4 text-sm text-gray-600">
|
<p class="text-gray-500 text-sm">Sentimen Negatif</p>
|
||||||
@foreach($totalPerWisata as $item)
|
<h3 class="text-2xl font-bold mt-2 text-red-600">{{ $stats['negatif_persen'] }}%</h3>
|
||||||
<div class="flex justify-between border-b pb-3">
|
</div>
|
||||||
<span>{{ $item->wisata }}</span>
|
<div class="bg-white rounded-xl shadow p-5">
|
||||||
<span class="font-semibold text-gray-800">
|
<p class="text-gray-500 text-sm">Sentimen Netral</p>
|
||||||
{{ $item->total }} ulasan
|
<h3 class="text-2xl font-bold mt-2 text-yellow-500">{{ $stats['netral_persen'] }}%</h3>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- WORD CLOUD -->
|
{{-- Chart Section --}}
|
||||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
<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>
|
{{-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- ================= ANALISIS ================= -->
|
|
||||||
<div x-show="tab === 'analisis'" id="analisis-content">
|
|
||||||
@include('analisis.index')
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================= REKOMENDASI ================= -->
|
</div>
|
||||||
<div x-show="tab === 'rekomendasi'" id="rekomendasi-content">
|
|
||||||
@include('rekomendasi.index')
|
{{-- 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>
|
||||||
|
|
||||||
</div>
|
{{-- SCRIPTS --}}
|
||||||
|
|
||||||
|
|
||||||
{{-- ========================= --}}
|
|
||||||
{{-- SCRIPT SECTION --}}
|
|
||||||
{{-- ========================= --}}
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<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 src="https://cdnjs.cloudflare.com/ajax/libs/wordcloud2.js/1.1.2/wordcloud2.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
// =============================
|
|
||||||
// PIE CHART
|
// PIE CHART
|
||||||
// =============================
|
|
||||||
const pieData = JSON.parse('{!! json_encode($chartSentimen) !!}');
|
const pieData = JSON.parse('{!! json_encode($chartSentimen) !!}');
|
||||||
|
const pieCanvas = document.getElementById('pieChart');
|
||||||
const pieLabels = pieData.map(item => item.sentimen);
|
if (window.pieChartInstance) window.pieChartInstance.destroy();
|
||||||
const pieValues = pieData.map(item => item.total);
|
window.pieChartInstance = new Chart(pieCanvas, {
|
||||||
|
|
||||||
new Chart(document.getElementById('pieChart'), {
|
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
labels: pieLabels,
|
labels: pieData.map(i => i.sentimen),
|
||||||
datasets: [{
|
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
|
// WORD CLOUD
|
||||||
// =============================
|
const text = `{!! addslashes($allText) !!}`;
|
||||||
const text = `{!! $allText !!}`;
|
if (text.length > 0) {
|
||||||
|
|
||||||
if(text.length > 0){
|
|
||||||
const words = text.split(" ");
|
|
||||||
|
|
||||||
const wordCount = {};
|
const wordCount = {};
|
||||||
words.forEach(word => {
|
text.split(" ").forEach(word => {
|
||||||
if(word.length > 3){
|
if (word.length > 3) wordCount[word] = (wordCount[word] || 0) + 1;
|
||||||
wordCount[word] = (wordCount[word] || 0) + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = Object.entries(wordCount);
|
|
||||||
|
|
||||||
WordCloud(document.getElementById('wordCloud'), {
|
|
||||||
list: list
|
|
||||||
});
|
});
|
||||||
|
WordCloud(document.getElementById('wordCloud'), { list: Object.entries(wordCount) });
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
|
||||||
const tab = new URLSearchParams(window.location.search).get('tab');
|
|
||||||
|
|
||||||
if (tab === 'analisis') {
|
|
||||||
showTab('analisis');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
const container = document.getElementById('analisis-content');
|
const container = document.getElementById('analisis-content');
|
||||||
|
|
||||||
// ================= FILTER AJAX =================
|
// FILTER AJAX
|
||||||
document.addEventListener('submit', function(e){
|
document.addEventListener('submit', function(e){
|
||||||
if(e.target.id === 'filter-form'){
|
if(e.target.id === 'filter-form'){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const params = new URLSearchParams(new FormData(e.target)).toString();
|
||||||
const formData = new FormData(e.target);
|
|
||||||
const params = new URLSearchParams(formData).toString();
|
|
||||||
|
|
||||||
fetch('/dashboard?' + params)
|
fetch('/dashboard?' + params)
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
const parser = new DOMParser();
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
||||||
|
|
||||||
const newContent = doc.getElementById('analisis-content');
|
|
||||||
container.innerHTML = newContent.innerHTML;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ================= PAGINATION AJAX =================
|
// PAGINATION AJAX
|
||||||
document.addEventListener('click', function(e){
|
document.addEventListener('click', function(e){
|
||||||
const link = e.target.closest('a');
|
const link = e.target.closest('a');
|
||||||
|
|
||||||
if(link && link.href.includes('page=')){
|
if(link && link.href.includes('page=')){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
fetch(link.href)
|
fetch(link.href)
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
const parser = new DOMParser();
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
||||||
|
|
||||||
const newContent = doc.getElementById('analisis-content');
|
|
||||||
container.innerHTML = newContent.innerHTML;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@endsection
|
@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">
|
<div class="space-y-6">
|
||||||
|
|
||||||
{{-- HEADER --}}
|
{{-- HEADER --}}
|
||||||
|
|
@ -9,26 +24,29 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
{{-- FILTER DESTINASI — submit ke /dashboard agar tab tidak reset --}}
|
||||||
<select class="border rounded-lg px-3 py-2 text-sm">
|
<form method="GET" action="{{ route('dashboard') }}" class="flex gap-3">
|
||||||
<option>Semua Destinasi</option>
|
<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>
|
||||||
|
</form>
|
||||||
<!-- <select class="border rounded-lg px-3 py-2 text-sm">
|
|
||||||
<option>2020 - 2025</option>
|
|
||||||
</select> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- SUMMARY --}}
|
{{-- SUMMARY CARDS --}}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-5">
|
<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-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 class="bg-red-100 text-red-500 p-3 rounded-full text-xl">😟</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-500">Total Ulasan Negatif</p>
|
<p class="text-sm text-gray-500">Total Ulasan Negatif</p>
|
||||||
<h3 class="text-xl font-bold">312</h3>
|
<h3 class="text-xl font-bold">{{ $totalNegatif }}</h3>
|
||||||
<p class="text-xs text-red-500">25,06% dari total</p>
|
<p class="text-xs text-red-500">{{ $persenNegatif }}% dari total</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -36,9 +54,12 @@
|
||||||
<div class="bg-green-100 text-green-600 p-3 rounded-full text-xl">😊</div>
|
<div class="bg-green-100 text-green-600 p-3 rounded-full text-xl">😊</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-500">Tingkat Kepuasan</p>
|
<p class="text-sm text-gray-500">Tingkat Kepuasan</p>
|
||||||
<h3 class="text-xl font-bold">68%</h3>
|
<h3 class="text-xl font-bold">{{ $tingkatKepuasan }}%</h3>
|
||||||
<span class="text-xs bg-yellow-100 text-yellow-700 px-2 py-1 rounded">
|
<span class="text-xs px-2 py-1 rounded
|
||||||
Sedang
|
{{ $labelKepuasan === 'Baik' ? 'bg-green-100 text-green-700' :
|
||||||
|
($labelKepuasan === 'Sedang' ? 'bg-yellow-100 text-yellow-700' :
|
||||||
|
'bg-red-100 text-red-700') }}">
|
||||||
|
{{ $labelKepuasan }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,20 +68,11 @@
|
||||||
<div class="bg-purple-100 text-purple-600 p-3 rounded-full text-xl">❗</div>
|
<div class="bg-purple-100 text-purple-600 p-3 rounded-full text-xl">❗</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-500">Isu Dominan</p>
|
<p class="text-sm text-gray-500">Isu Dominan</p>
|
||||||
<h3 class="text-xl font-bold">Kebersihan</h3>
|
<h3 class="text-xl font-bold">{{ $isuDominan }}</h3>
|
||||||
<p class="text-xs text-gray-500">45% dari total isu</p>
|
<p class="text-xs text-gray-500">{{ $isuDominanPersen }}% dari total isu</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{{-- GRID ANALISIS --}}
|
{{-- GRID ANALISIS --}}
|
||||||
|
|
@ -70,158 +82,120 @@
|
||||||
<div class="bg-white rounded-xl shadow p-5">
|
<div class="bg-white rounded-xl shadow p-5">
|
||||||
<h3 class="font-semibold mb-4 text-gray-700">Isu Utama (Top 5)</h3>
|
<h3 class="font-semibold mb-4 text-gray-700">Isu Utama (Top 5)</h3>
|
||||||
|
|
||||||
@php
|
@forelse ($isuUtama as $i => $isu)
|
||||||
$isu = [
|
@php
|
||||||
['nama'=>'Kebersihan','persen'=>45,'warna'=>'red'],
|
$warnaClass = match($isu['color']) {
|
||||||
['nama'=>'Parkir','persen'=>30,'warna'=>'orange'],
|
'red' => 'bg-red-500',
|
||||||
['nama'=>'Harga / Tiket','persen'=>15,'warna'=>'yellow'],
|
'orange' => 'bg-orange-500',
|
||||||
['nama'=>'Fasilitas','persen'=>6,'warna'=>'green'],
|
'yellow' => 'bg-yellow-400',
|
||||||
['nama'=>'Keramaian','persen'=>4,'warna'=>'blue'],
|
'green' => 'bg-green-500',
|
||||||
];
|
default => 'bg-blue-500',
|
||||||
@endphp
|
};
|
||||||
|
@endphp
|
||||||
@foreach($isu as $i => $item)
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="flex justify-between text-sm mb-1">
|
<div class="flex justify-between text-sm mb-1">
|
||||||
<span>{{ $i+1 }}. {{ $item['nama'] }}</span>
|
<span>{{ $i + 1 }}. {{ $isu['nama'] }}</span>
|
||||||
<span>{{ $item['persen'] }}%</span>
|
<span class="font-semibold">{{ $isu['persen'] }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-200 h-2 rounded">
|
<div class="w-full bg-gray-200 h-2 rounded">
|
||||||
<div class="h-2 rounded
|
<div class="h-2 rounded {{ $warnaClass }}" style="width: {{ $isu['persen'] }}%"></div>
|
||||||
@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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@empty
|
||||||
|
<p class="text-sm text-gray-400 text-center py-4">Belum ada data isu.</p>
|
||||||
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- KEYWORD --}}
|
{{-- KATA KUNCI DOMINAN --}}
|
||||||
<div class="bg-white rounded-xl shadow p-5">
|
<div class="bg-white rounded-xl shadow p-5">
|
||||||
<h3 class="font-semibold mb-4 text-gray-700">Kata Kunci Dominan</h3>
|
<h3 class="font-semibold mb-4 text-gray-700">Kata Kunci Dominan</h3>
|
||||||
|
|
||||||
@php
|
@php $maxFreq = !empty($kataDominan) ? max($kataDominan) : 1; @endphp
|
||||||
$keywords = [
|
|
||||||
['kotor',15],
|
|
||||||
['parkir',10],
|
|
||||||
['mahal',7],
|
|
||||||
['sampah',6],
|
|
||||||
['toilet',5],
|
|
||||||
];
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
@foreach($keywords as $k)
|
@forelse (array_slice($kataDominan, 0, 5, true) as $kata => $jumlah)
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex justify-between text-sm">
|
||||||
<span>{{ $k[0] }}</span>
|
<span>{{ $kata }}</span>
|
||||||
<span>{{ $k[1] }}x</span>
|
<span class="font-semibold text-blue-600">{{ $jumlah }}x</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-gray-200 h-2 rounded mt-1">
|
<div class="w-full bg-gray-200 h-2 rounded mt-1">
|
||||||
<div class="bg-blue-500 h-2 rounded"
|
<div class="bg-blue-500 h-2 rounded" style="width: {{ round(($jumlah / $maxFreq) * 100) }}%"></div>
|
||||||
style="width: {{ $k[1]*5 }}%"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@empty
|
||||||
|
<p class="text-sm text-gray-400 text-center py-4">Belum ada kata kunci.</p>
|
||||||
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- SARAN --}}
|
{{-- SARAN PERBAIKAN --}}
|
||||||
<div class="bg-white rounded-xl shadow p-5">
|
<div class="bg-white rounded-xl shadow p-5">
|
||||||
<h3 class="font-semibold mb-4 text-gray-700">Saran Perbaikan</h3>
|
<h3 class="font-semibold mb-4 text-gray-700">Saran Perbaikan</h3>
|
||||||
|
|
||||||
<div class="space-y-4 text-sm">
|
<div class="space-y-4 text-sm">
|
||||||
|
@forelse ($saranPerbaikan as $saran)
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<div class="text-green-500 text-xl">🧹</div>
|
<div class="text-xl">{{ $saran['icon'] }}</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold">Perbaikan Kebersihan</p>
|
<p class="font-semibold">{{ $saran['nama'] }}</p>
|
||||||
<p class="text-gray-600">
|
<p class="text-gray-600">{{ $saran['tip'] }}</p>
|
||||||
Tambah tempat sampah & jadwal pembersihan rutin.
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@empty
|
||||||
|
<p class="text-sm text-gray-400 text-center py-4">Belum ada saran.</p>
|
||||||
<div class="flex gap-3">
|
@endforelse
|
||||||
<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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- PRIORITAS --}}
|
{{-- PRIORITAS REKOMENDASI --}}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
|
||||||
{{-- CARD 1 --}}
|
@forelse ($prioritas as $p)
|
||||||
<div class="border border-red-300 rounded-xl p-5 bg-white">
|
@php
|
||||||
<h4 class="font-semibold text-red-600 mb-2">Prioritas 1</h4>
|
$border = match($p['color']) {
|
||||||
<h3 class="text-lg font-bold mb-3">Kebersihan</h3>
|
'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">
|
<ul class="text-sm space-y-2 mb-4">
|
||||||
<li>✔ Tambah tempat sampah</li>
|
@foreach ($p['actions'] as $action)
|
||||||
<li>✔ Jadwal pembersihan rutin</li>
|
<li class="flex items-center gap-2">
|
||||||
<li>✔ Petugas kebersihan tambahan</li>
|
<span class="text-blue-500">✔</span> {{ $action }}
|
||||||
</ul>
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="mt-4 text-xs bg-red-100 text-red-600 p-2 rounded">
|
<div class="text-xs {{ $dampak }} p-2 rounded">
|
||||||
Dampak: Kepuasan meningkat signifikan
|
Dampak: {{ $p['dampak'] }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@empty
|
||||||
|
<div class="col-span-3 text-center text-gray-400 text-sm py-6">
|
||||||
{{-- CARD 2 --}}
|
Belum ada data rekomendasi. Pastikan analisis sentimen sudah dijalankan.
|
||||||
<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
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endforelse
|
||||||
|
|
||||||
{{-- 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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,68 +5,56 @@
|
||||||
<div class="max-w-7xl mx-auto px-4 md:px-0 space-y-6">
|
<div class="max-w-7xl mx-auto px-4 md:px-0 space-y-6">
|
||||||
|
|
||||||
<!-- HEADER -->
|
<!-- 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>
|
||||||
|
|
||||||
<!-- ============================= -->
|
</div>
|
||||||
<!-- 🔥 UPLOAD + FORMAT + SUMMARY -->
|
|
||||||
<!-- ============================= -->
|
|
||||||
<div class="bg-white rounded-xl shadow p-6">
|
|
||||||
|
|
||||||
|
<!-- 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">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 items-center">
|
||||||
|
|
||||||
<!-- UPLOAD -->
|
<!-- UPLOAD -->
|
||||||
<div class="border-2 border-dashed border-blue-300 rounded-xl p-6 text-center">
|
<div class="border-2 border-dashed border-blue-300 rounded-xl p-6 text-center">
|
||||||
|
|
||||||
<div class="flex justify-center mb-3">
|
<div class="flex justify-center mb-3">
|
||||||
<div class="bg-blue-100 text-blue-600 p-3 rounded-full text-xl">
|
<div class="bg-blue-100 text-blue-600 p-3 rounded-full text-xl">☁️</div>
|
||||||
☁️
|
|
||||||
</div>
|
|
||||||
</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>
|
<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">
|
<form id="uploadForm" action="{{ route('ulasan.upload') }}" method="POST" enctype="multipart/form-data">
|
||||||
@csrf
|
@csrf
|
||||||
|
<input type="file" name="file" id="fileInput" accept=".csv,.xlsx,.xls" class="hidden">
|
||||||
<input type="file" name="file" id="fileInput"
|
<button type="button" onclick="document.getElementById('fileInput').click()"
|
||||||
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">
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm shadow">
|
||||||
Pilih File
|
Pilih File
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- <a href="#" class="block mt-3 text-blue-500 text-xs hover:underline">
|
|
||||||
⬇ Download template CSV
|
|
||||||
</a> -->
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- FORMAT -->
|
<!-- FORMAT -->
|
||||||
<div class="bg-blue-50 rounded-xl p-5">
|
<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">
|
<ul class="space-y-2 text-sm text-gray-700">
|
||||||
<li>✔ wisata, rating, ulasan, tanggal</li>
|
<li>✔ wisata, rating, ulasan, tanggal</li>
|
||||||
<li>✔ Format: YYYY-MM-DD HH:MM:SS</li>
|
<li>✔ Format: YYYY-MM-DD HH:MM:SS</li>
|
||||||
<li>✔ Maks 10MB</li>
|
<li>✔ Maks 10MB</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SUMMARY -->
|
<!-- SUMMARY -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="bg-blue-100 text-blue-600 p-3 rounded-full">📄</div>
|
<div class="bg-blue-100 text-blue-600 p-3 rounded-full">📄</div>
|
||||||
<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>
|
<p class="font-bold text-lg">{{ $totalUlasan }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="bg-green-100 text-green-600 p-3 rounded-full">📍</div>
|
<div class="bg-green-100 text-green-600 p-3 rounded-full">📍</div>
|
||||||
<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>
|
<p class="font-bold text-lg">{{ $totalWisata }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="bg-orange-100 text-orange-600 p-3 rounded-full">📅</div>
|
<div class="bg-orange-100 text-orange-600 p-3 rounded-full">📅</div>
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ALERT -->
|
<!-- 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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- ============================= -->
|
<!-- FILTER PERIODE + SEARCH -->
|
||||||
<!-- 🔍 SEARCH + FILTER -->
|
<form method="GET" action="{{ route('ulasan.index') }}" class="flex flex-col md:flex-row gap-3 items-center">
|
||||||
<!-- ============================= -->
|
|
||||||
<form method="GET" action="{{ route('ulasan.index') }}" class="flex flex-col md:flex-row gap-3">
|
|
||||||
|
|
||||||
|
{{-- 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') }}"
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
placeholder="Cari ulasan atau wisata..."
|
placeholder="Cari ulasan atau wisata..."
|
||||||
class="flex-1 border px-3 py-2 rounded-lg text-sm">
|
class="flex-1 border px-3 py-2 rounded-lg text-sm">
|
||||||
|
|
||||||
<!-- <select name="wisata" onchange="this.form.submit()"
|
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm">Cari</button>
|
||||||
class="border rounded-lg px-3 py-2 text-sm">
|
|
||||||
|
|
||||||
<option value="">Semua Destinasi</option>
|
<a href="{{ route('ulasan.index') }}" class="bg-gray-200 px-4 py-2 rounded-lg text-sm">Reset</a>
|
||||||
<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>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- ============================= -->
|
|
||||||
<!-- TABLE -->
|
<!-- TABLE -->
|
||||||
<!-- ============================= -->
|
|
||||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
<table class="w-full text-sm">
|
<thead class="bg-gray-100">
|
||||||
<thead class="bg-gray-100">
|
<tr>
|
||||||
<tr>
|
<th class="px-4 py-3 w-[5%]">No</th>
|
||||||
<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-[20%]">Nama Wisata</th>
|
<th class="px-4 py-3 w-[10%] text-center">Rating</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-[45%]">Ulasan</th>
|
<th class="px-4 py-3 w-[20%] text-center">Tanggal</th>
|
||||||
<th class="px-4 py-3 w-[20%] text-center">Tanggal</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody class="divide-y">
|
||||||
|
@forelse($mentah as $i => $item)
|
||||||
<tbody class="divide-y">
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3">{{ $mentah->firstItem() + $i }}</td>
|
||||||
@forelse($mentah as $i => $item)
|
<td class="px-4 py-3 font-medium">{{ $item->wisata }}</td>
|
||||||
<tr class="hover:bg-gray-50">
|
<td class="px-4 py-3 text-center text-yellow-500">
|
||||||
|
{{ str_repeat('★', round($item->rating)) }}
|
||||||
<td class="px-4 py-3">{{ $mentah->firstItem() + $i }}</td>
|
<span class="text-gray-500 ml-1">{{ $item->rating }}</span>
|
||||||
|
</td>
|
||||||
<td class="px-4 py-3 font-medium">
|
<td class="px-4 py-3">
|
||||||
{{ $item->wisata }}
|
<div class="line-clamp-2 text-gray-600">{{ $item->ulasan }}</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center text-gray-500">
|
||||||
<td class="px-4 py-3 text-center text-yellow-500">
|
{{ \Carbon\Carbon::parse($item->tanggal)->diffForHumans() }}
|
||||||
{{ str_repeat('★', round($item->rating)) }}
|
</td>
|
||||||
<span class="text-gray-500 ml-1">{{ $item->rating }}</span>
|
</tr>
|
||||||
</td>
|
@empty
|
||||||
|
<tr>
|
||||||
<td class="px-4 py-3">
|
<td colspan="5" class="text-center py-6 text-gray-400">Belum ada data</td>
|
||||||
<div class="line-clamp-2 text-gray-600">
|
</tr>
|
||||||
{{ $item->ulasan }}
|
@endforelse
|
||||||
</div>
|
</tbody>
|
||||||
</td>
|
</table>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 flex flex-col md:flex-row gap-3 justify-between items-center text-sm text-gray-500">
|
<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) -->
|
@for ($i = 1; $i <= $mentah->lastPage(); $i++)
|
||||||
<div>
|
@if ($i == $mentah->currentPage())
|
||||||
Menampilkan {{ $mentah->firstItem() ?? 0 }} - {{ $mentah->lastItem() ?? 0 }}
|
<span class="px-3 py-1 bg-blue-600 text-white rounded-lg">{{ $i }}</span>
|
||||||
dari {{ $mentah->total() }} data
|
@elseif ($i <= 3 || $i > $mentah->lastPage() - 2 || abs($i - $mentah->currentPage()) <= 1)
|
||||||
</div>
|
<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) -->
|
@if ($mentah->hasMorePages())
|
||||||
<div class="flex items-center gap-1">
|
<a href="{{ $mentah->nextPageUrl() }}" class="px-3 py-1 bg-gray-100 rounded-lg">»</a>
|
||||||
|
@else
|
||||||
@if ($mentah->onFirstPage())
|
<span class="px-3 py-1 bg-gray-200 rounded-lg">»</span>
|
||||||
<span class="px-3 py-1 bg-gray-200 rounded-lg">«</span>
|
@endif
|
||||||
@else
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const input = document.getElementById("fileInput");
|
const input = document.getElementById("fileInput");
|
||||||
const form = document.getElementById("uploadForm");
|
const form = document.getElementById("uploadForm");
|
||||||
|
|
||||||
input.addEventListener("change", function () {
|
input.addEventListener("change", function () {
|
||||||
form.submit();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,42 +7,61 @@
|
||||||
use App\Http\Controllers\AnalisisController;
|
use App\Http\Controllers\AnalisisController;
|
||||||
use App\Http\Controllers\RekomendasiController;
|
use App\Http\Controllers\RekomendasiController;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| ROUTE AWAL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
// hanya SATU route '/'
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return redirect('/login');
|
return redirect('/login');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/', function () {
|
/*
|
||||||
return view('welcome');
|
|--------------------------------------------------------------------------
|
||||||
});
|
| ROUTE SETELAH LOGIN
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
// Dashboard SENTARA (harus login)
|
Route::middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::get('/dashboard', [DashboardController::class, 'index'])
|
|
||||||
->middleware(['auth', 'verified'])
|
|
||||||
->name('dashboard');
|
|
||||||
|
|
||||||
// Route::get('/dashboard', function () {
|
// ✅ DASHBOARD
|
||||||
// return view('dashboard');
|
Route::get('/dashboard', [DashboardController::class, 'index'])
|
||||||
// })->middleware(['auth', 'verified'])->name('dashboard');
|
->name('dashboard');
|
||||||
|
|
||||||
Route::middleware('auth')->group(function () {
|
// ✅ PROFILE (ini yang kamu tanya)
|
||||||
Route::get('/profile', function () {
|
Route::get('/profile', function () {
|
||||||
return view('profile.index');
|
return view('profile.index');
|
||||||
})->name('profile');
|
})->name('profile');
|
||||||
|
|
||||||
|
// kalau mau aktifkan update:
|
||||||
// Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
// Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
// Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
// 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');
|
// ✅ DATA ULASAN
|
||||||
Route::post('/ulasan/analisis', [UlasanController::class, 'analisisData'])->name('ulasan.analisis');
|
Route::get('/ulasan', [UlasanController::class, 'index'])
|
||||||
Route::post('/ulasan/upload', [UlasanController::class, 'upload'])->name('ulasan.upload');
|
->name('ulasan.index');
|
||||||
Route::get('/hasil-analisis', [AnalisisController::class, 'index'])
|
|
||||||
->name('analisis.index');
|
// Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])
|
||||||
Route::get('/rekomendasi', [RekomendasiController::class, 'index'])
|
// ->name('ulasan.ambil');
|
||||||
->name('rekomendasi.index');;
|
|
||||||
Route::get('/', function () {
|
Route::post('/ulasan/upload', [UlasanController::class, 'upload'])
|
||||||
return redirect()->route('login');
|
->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 re
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pymysql
|
import pymysql
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from sklearn.model_selection import train_test_split
|
from sklearn.model_selection import train_test_split
|
||||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
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.Stemmer.StemmerFactory import StemmerFactory
|
||||||
from Sastrawi.StopWordRemover.StopWordRemoverFactory import StopWordRemoverFactory
|
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',
|
host='localhost',
|
||||||
user='root',
|
user='root',
|
||||||
password='',
|
password='',
|
||||||
database='sentara',
|
database='sentara',
|
||||||
cursorclass=pymysql.cursors.DictCursor
|
cursorclass=pymysql.cursors.DictCursor
|
||||||
)
|
)
|
||||||
|
cursor = raw_conn.cursor()
|
||||||
cursor = 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 = df.dropna(subset=["ulasan"])
|
||||||
df["ulasan"] = df["ulasan"].astype(str)
|
df["ulasan"] = df["ulasan"].astype(str)
|
||||||
df = df[df["ulasan"].str.strip() != ""]
|
df = df[df["ulasan"].str.strip() != ""]
|
||||||
|
df = df[df["ulasan"].str.strip() != "0"]
|
||||||
|
|
||||||
# ===============================
|
# ===============================
|
||||||
# 3. PREPROCESSING
|
# 4. PREPROCESSING
|
||||||
# ===============================
|
# ===============================
|
||||||
stemmer = StemmerFactory().create_stemmer()
|
stemmer = StemmerFactory().create_stemmer()
|
||||||
stop_factory = StopWordRemoverFactory()
|
stop_factory = StopWordRemoverFactory()
|
||||||
|
|
@ -46,37 +64,57 @@ def clean_text(text):
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
text = re.sub(r"http\S+", " ", text)
|
text = re.sub(r"http\S+", " ", text)
|
||||||
text = re.sub(r"[^a-zA-Z\s]", " ", text)
|
text = re.sub(r"[^a-zA-Z\s]", " ", text)
|
||||||
text = re.sub(r"\s+", " ", text)
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
words = [w for w in text.split() if w not in stopwords and len(w) > 2]
|
||||||
words = [w for w in text.split() if w not in stopwords]
|
|
||||||
return stemmer.stem(" ".join(words))
|
return stemmer.stem(" ".join(words))
|
||||||
|
|
||||||
df["clean"] = df["ulasan"].apply(clean_text)
|
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):
|
def label_rule(text):
|
||||||
if "tidak" in text or "buruk" in text:
|
words = set(text.split())
|
||||||
return "negatif"
|
skor_pos = len(words & positif_words)
|
||||||
elif "bagus" in text or "indah" in text:
|
skor_neg = len(words & negatif_words)
|
||||||
|
if skor_pos > skor_neg:
|
||||||
return "positif"
|
return "positif"
|
||||||
|
elif skor_neg > skor_pos:
|
||||||
|
return "negatif"
|
||||||
else:
|
else:
|
||||||
return "netral"
|
return "netral"
|
||||||
|
|
||||||
df["label"] = df["clean"].apply(label_rule)
|
df["label"] = df["clean"].apply(label_rule)
|
||||||
|
|
||||||
# ===============================
|
# ===============================
|
||||||
# 5. TF-IDF + MODEL
|
# 6. TF-IDF + MODEL
|
||||||
# ===============================
|
# ===============================
|
||||||
X = df["clean"]
|
X = df["clean"]
|
||||||
y = df["label"]
|
y = df["label"]
|
||||||
|
|
||||||
X_train, X_test, y_train, y_test = train_test_split(
|
if len(df) > 5:
|
||||||
X, y, test_size=0.2, random_state=42, stratify=y
|
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_train_vec = vectorizer.fit_transform(X_train)
|
||||||
X_test_vec = vectorizer.transform(X_test)
|
X_test_vec = vectorizer.transform(X_test)
|
||||||
|
|
||||||
|
|
@ -84,59 +122,65 @@ model = ComplementNB()
|
||||||
model.fit(X_train_vec, y_train)
|
model.fit(X_train_vec, y_train)
|
||||||
|
|
||||||
# ===============================
|
# ===============================
|
||||||
# 6. EVALUASI
|
# 7. EVALUASI
|
||||||
# ===============================
|
# ===============================
|
||||||
y_pred = model.predict(X_test_vec)
|
y_pred = model.predict(X_test_vec)
|
||||||
|
|
||||||
acc = accuracy_score(y_test, y_pred)
|
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"])
|
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"])
|
X_all = vectorizer.transform(df["clean"])
|
||||||
df["prediksi"] = model.predict(X_all)
|
df["prediksi"] = model.predict(X_all)
|
||||||
|
|
||||||
# ===============================
|
# ===============================
|
||||||
# 8. SIMPAN KE DB
|
# 9. SIMPAN KE DB
|
||||||
# ===============================
|
# ===============================
|
||||||
|
df = df.fillna("")
|
||||||
|
|
||||||
# kosongkan tabel
|
# Hapus data periode ini saja (bukan semua)
|
||||||
cursor.execute("DELETE FROM hasil_analisis")
|
cursor.execute("DELETE FROM hasil_analisis WHERE periode_id = %s", (periode_id,))
|
||||||
cursor.execute("DELETE FROM evaluasi_model")
|
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():
|
for _, row in df.iterrows():
|
||||||
cursor.execute("""
|
cursor.execute(insert_query, (
|
||||||
INSERT INTO hasil_analisis
|
str(row["wisata"]),
|
||||||
(wisata, ulasan_terolah, hasil_preprocessing, sentimen, probabilitas, created_at)
|
str(row["ulasan"]),
|
||||||
VALUES (%s,%s,%s,%s,%s,NOW())
|
str(row["clean"]),
|
||||||
""", (
|
str(row["clean"]),
|
||||||
row["wisata"],
|
str(row["prediksi"]),
|
||||||
row["ulasan"],
|
float(0.9),
|
||||||
row["clean"],
|
periode_id
|
||||||
row["prediksi"],
|
|
||||||
0.9
|
|
||||||
))
|
))
|
||||||
|
|
||||||
# simpan evaluasi
|
print(f"[OK] {len(df)} ulasan berhasil disimpan untuk periode {periode_nama}")
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO evaluasi_model
|
INSERT INTO evaluasi_model
|
||||||
(precision, recall, f1_score, accuracy, tp, tn, fp, fn, created_at)
|
(`precision`, `recall`, f1_score, accuracy, tp, tn, fp, fn, periode_id)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
""", (
|
""", (
|
||||||
report["weighted avg"]["precision"],
|
float(report["weighted avg"]["precision"]),
|
||||||
report["weighted avg"]["recall"],
|
float(report["weighted avg"]["recall"]),
|
||||||
report["weighted avg"]["f1-score"],
|
float(report["weighted avg"]["f1-score"]),
|
||||||
acc,
|
float(acc),
|
||||||
int(cm[2][2]),
|
int(cm[2][2]) if cm.shape == (3,3) else 0,
|
||||||
int(cm[0][0]),
|
int(cm[0][0]) if cm.shape == (3,3) else 0,
|
||||||
int(cm[0][2]),
|
int(cm[0][2]) if cm.shape == (3,3) else 0,
|
||||||
int(cm[2][0])
|
int(cm[2][0]) if cm.shape == (3,3) else 0,
|
||||||
|
periode_id
|
||||||
))
|
))
|
||||||
|
|
||||||
conn.commit()
|
raw_conn.commit()
|
||||||
conn.close()
|
raw_conn.close()
|
||||||
|
|
||||||
print("Analisis selesai ✔")
|
print("Analisis selesai [OK]")
|
||||||
Loading…
Reference in New Issue