339 lines
14 KiB
PHP
339 lines
14 KiB
PHP
@extends('layouts.sentara')
|
|
|
|
@section('content')
|
|
|
|
<div class="max-w-7xl mx-auto">
|
|
|
|
{{-- ALERT --}}
|
|
@if(session('success'))
|
|
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded">
|
|
{{ session('success') }}
|
|
</div>
|
|
@endif
|
|
@if(session('error'))
|
|
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded whitespace-pre-line">
|
|
{{ session('error') }}
|
|
</div>
|
|
@endif
|
|
@if($periodeAktif && !$hasAnalisis)
|
|
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-6 rounded">
|
|
Data ulasan sudah tersedia, tetapi belum dilakukan analisis sentimen.
|
|
</div>
|
|
@endif
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm text-gray-600 font-medium">
|
|
Periode aktif: {{ $periodeAktif->nama ?? 'Belum ada data' }}
|
|
</span>
|
|
<a href="{{ route('riwayat.index') }}" class="text-sm text-blue-600 hover:underline">
|
|
Lihat riwayat
|
|
</a>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if($periodeAktif)
|
|
<span class="text-xs text-gray-400">
|
|
Terakhir update: {{ $lastUpdate ? \Carbon\Carbon::parse($lastUpdate)->format('d M Y H:i') : '-' }}
|
|
</span>
|
|
@endif
|
|
<form action="{{ route('ulasan.ambil') }}" method="POST" class="flex flex-col md:flex-row gap-3 md:items-center">
|
|
@csrf
|
|
<input type="hidden" name="redirect_to" value="dashboard">
|
|
<input type="month" name="periode_bulan" value="{{ now()->format('Y-m') }}"
|
|
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-[150px]">
|
|
<select name="wisata" class="border rounded-lg pl-3 pr-9 py-2 text-sm bg-white shadow-sm min-w-[220px]">
|
|
<option value="">Semua Lokasi</option>
|
|
@foreach(($scraperDestinations ?? []) as $scraperDestination)
|
|
<option value="{{ $scraperDestination }}">{{ $scraperDestination }}</option>
|
|
@endforeach
|
|
</select>
|
|
<button class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm shadow">
|
|
Ambil Data
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-data="{ tab: new URLSearchParams(window.location.search).get('tab') || 'dashboard' }" class="mb-6">
|
|
|
|
<!-- TAB -->
|
|
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
|
<button @click="tab='dashboard'"
|
|
:class="tab==='dashboard' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
|
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
|
Dashboard Sentara
|
|
</button>
|
|
<button @click="tab='analisis'"
|
|
:class="tab==='analisis' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
|
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
|
Hasil Analisis Sentimen
|
|
</button>
|
|
<button @click="tab='rekomendasi'"
|
|
:class="tab==='rekomendasi' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
|
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
|
Rekomendasi Layanan
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ================= DASHBOARD ================= -->
|
|
<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 class="bg-white rounded-xl shadow p-5">
|
|
<p class="text-gray-500 text-sm">Sentimen Positif</p>
|
|
<h3 class="text-2xl font-bold mt-2 text-green-600">{{ $stats['positif_persen'] }}%</h3>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow p-5">
|
|
<p class="text-gray-500 text-sm">Sentimen Negatif</p>
|
|
<h3 class="text-2xl font-bold mt-2 text-red-600">{{ $stats['negatif_persen'] }}%</h3>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow p-5">
|
|
<p class="text-gray-500 text-sm">Sentimen Netral</p>
|
|
<h3 class="text-2xl font-bold mt-2 text-yellow-500">{{ $stats['netral_persen'] }}%</h3>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Chart Section --}}
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
|
|
{{-- 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 --}}
|
|
<div class="bg-white rounded-xl shadow p-6">
|
|
<h3 class="font-semibold mb-4 text-gray-700">Sentimen per Destinasi</h3>
|
|
<div class="h-[300px]">
|
|
<canvas id="destinationChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{{-- Section Bawah --}}
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
|
<h3 class="font-semibold mb-6 text-gray-700">Total Data per Destinasi</h3>
|
|
<div class="space-y-4 text-sm text-gray-600">
|
|
@foreach($totalPerWisata as $item)
|
|
<div class="flex justify-between border-b pb-3">
|
|
<span>{{ $item->wisata }}</span>
|
|
<span class="font-semibold text-gray-800">{{ $item->total }} ulasan</span>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
|
<h3 class="font-semibold mb-6 text-gray-700">Word Cloud</h3>
|
|
<div id="wordCloud" class="w-full h-[240px] overflow-hidden flex flex-wrap content-center justify-center gap-3 text-center">
|
|
<p class="text-sm text-gray-400">Belum ada kata dominan.</p>
|
|
</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>
|
|
|
|
{{-- SCRIPTS --}}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
// PIE CHART
|
|
const pieData = JSON.parse('{!! json_encode($chartSentimen) !!}');
|
|
const pieCanvas = document.getElementById('pieChart');
|
|
if (window.pieChartInstance) window.pieChartInstance.destroy();
|
|
window.pieChartInstance = new Chart(pieCanvas, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: pieData.map(i => i.sentimen),
|
|
datasets: [{
|
|
data: pieData.map(i => i.total),
|
|
backgroundColor: ['#3B82F6', '#EF4444', '#F59E0B']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false
|
|
}
|
|
});
|
|
|
|
// BAR CHART SENTIMEN PER DESTINASI
|
|
const destinationData = @json($chartDestinasi);
|
|
const destinationCanvas = document.getElementById('destinationChart');
|
|
if (destinationCanvas) {
|
|
if (window.destinationChartInstance) window.destinationChartInstance.destroy();
|
|
window.destinationChartInstance = new Chart(destinationCanvas, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: destinationData.map(item => item.wisata),
|
|
datasets: [
|
|
{
|
|
label: 'Positif',
|
|
data: destinationData.map(item => Number(item.positif || 0)),
|
|
backgroundColor: 'rgba(96, 165, 250, 0.88)',
|
|
borderColor: '#3B82F6',
|
|
borderWidth: 1,
|
|
borderRadius: 3,
|
|
},
|
|
{
|
|
label: 'Negatif',
|
|
data: destinationData.map(item => Number(item.negatif || 0)),
|
|
backgroundColor: 'rgba(248, 113, 113, 0.9)',
|
|
borderColor: '#EF4444',
|
|
borderWidth: 1,
|
|
borderRadius: 3,
|
|
},
|
|
{
|
|
label: 'Netral',
|
|
data: destinationData.map(item => Number(item.netral || 0)),
|
|
backgroundColor: 'rgba(251, 191, 36, 0.72)',
|
|
borderColor: '#F59E0B',
|
|
borderWidth: 1,
|
|
borderRadius: 3,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
align: 'center',
|
|
labels: {
|
|
boxWidth: 38,
|
|
boxHeight: 10,
|
|
color: '#4B5563',
|
|
font: { size: 12 },
|
|
},
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: context => `${context.dataset.label}: ${context.parsed.y} ulasan`,
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: { color: 'rgba(156, 163, 175, 0.22)' },
|
|
ticks: {
|
|
color: '#4B5563',
|
|
maxRotation: 14,
|
|
minRotation: 14,
|
|
font: { size: 12 },
|
|
},
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
color: '#4B5563',
|
|
precision: 0,
|
|
},
|
|
grid: { color: 'rgba(156, 163, 175, 0.28)' },
|
|
},
|
|
},
|
|
categoryPercentage: 0.72,
|
|
barPercentage: 0.82,
|
|
},
|
|
});
|
|
}
|
|
|
|
// WORD CLOUD tanpa library eksternal agar tetap tampil stabil.
|
|
const text = @json($allText);
|
|
const wordCloud = document.getElementById('wordCloud');
|
|
const stopwords = new Set([
|
|
'yang','dan','dari','untuk','dengan','ini','itu','tidak','ada','juga',
|
|
'sangat','lebih','sudah','bisa','tapi','karena','pada','atau','saya',
|
|
'kami','mereka','nya','jadi','kalau','dalam','akan','saat','tempat'
|
|
]);
|
|
|
|
if (wordCloud && text.trim().length > 0) {
|
|
const wordCount = {};
|
|
text.toLowerCase().split(/\s+/).forEach(word => {
|
|
const cleanWord = word.replace(/[^a-z]/g, '');
|
|
if (cleanWord.length > 3 && !stopwords.has(cleanWord)) {
|
|
wordCount[cleanWord] = (wordCount[cleanWord] || 0) + 1;
|
|
}
|
|
});
|
|
|
|
const words = Object.entries(wordCount)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 28);
|
|
|
|
if (words.length > 0) {
|
|
const max = words[0][1] || 1;
|
|
const colors = ['text-blue-700', 'text-green-600', 'text-red-500', 'text-yellow-600', 'text-indigo-600'];
|
|
wordCloud.innerHTML = words.map(([word, count], index) => {
|
|
const size = 13 + Math.round((count / max) * 20);
|
|
const weight = count === max ? 800 : 600;
|
|
return `<span class="${colors[index % colors.length]} inline-block" style="font-size:${size}px;font-weight:${weight}" title="${count}x">${word}</span>`;
|
|
}).join('');
|
|
}
|
|
}
|
|
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
const container = document.getElementById('analisis-content');
|
|
|
|
// FILTER AJAX
|
|
document.addEventListener('submit', function(e){
|
|
if(e.target.id === 'filter-form'){
|
|
e.preventDefault();
|
|
const params = new URLSearchParams(new FormData(e.target)).toString();
|
|
fetch('/dashboard?' + params)
|
|
.then(res => res.text())
|
|
.then(html => {
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
|
});
|
|
}
|
|
});
|
|
|
|
// PAGINATION AJAX
|
|
document.addEventListener('click', function(e){
|
|
const link = e.target.closest('a');
|
|
if(link && link.href.includes('page=')){
|
|
e.preventDefault();
|
|
fetch(link.href)
|
|
.then(res => res.text())
|
|
.then(html => {
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
|
});
|
|
}
|
|
});
|
|
|
|
});
|
|
</script>
|
|
|
|
@endsection
|