bismillah

This commit is contained in:
MufridaFaraDiani27 2026-05-26 04:04:37 +07:00
parent f24f8652bf
commit c2e22b1dae
51 changed files with 9872 additions and 143 deletions

View File

@ -67,4 +67,4 @@ public function index(Request $request)
'totalHasilAnalisis'
));
}
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Barryvdh\DomPDF\Facade\Pdf;
class LaporanController extends Controller
{
// ─────────────────────────────────────────────
// Laporan BULANAN → /laporan/bulanan?periode_id=X
// ─────────────────────────────────────────────
public function downloadBulanan(Request $request, $periode)
{
$periodeId = $periode;
// Ambil info periode
$periode = DB::table('periode_analisis')->where('id', $periodeId)->first();
if (!$periode) {
return back()->with('error', 'Periode tidak ditemukan.');
}
$data = $this->collectData($periodeId);
$data['periode'] = $periode;
$data['judulLaporan'] = 'Laporan Analisis Sentimen — ' . $periode->nama;
$pdf = Pdf::loadView('laporan.bulanan', $data)
->setPaper('a4', 'portrait')
->setOptions(['dpi' => 110, 'defaultFont' => 'sans-serif', 'isHtml5ParserEnabled' => true]);
$filename = 'laporan-sentimen-' . strtolower(str_replace(' ', '-', $periode->nama)) . '.pdf';
return $pdf->download($filename);
}
// ─────────────────────────────────────────────
// Laporan TAHUNAN → /laporan/tahunan?tahun=2025
// ─────────────────────────────────────────────
public function downloadTahunan(Request $request, $tahun)
{
$tahun = $request->input('tahun', now()->year);
$periodeList = DB::table('periode_analisis')
->where('tahun', $tahun)
->orderBy('bulan')
->get();
if ($periodeList->isEmpty()) {
return back()->with('error', "Tidak ada data untuk tahun $tahun.");
}
// Kumpulkan data per bulan
$dataBulanan = [];
foreach ($periodeList as $periode) {
$dataBulanan[] = array_merge(
['periode' => $periode],
$this->collectData($periode->id)
);
}
// Agregat tahunan
$totalUlasan = collect($dataBulanan)->sum(fn($d) => $d['totalUlasan']);
$totalPositif = collect($dataBulanan)->sum(fn($d) => $d['totalPositif']);
$totalNegatif = collect($dataBulanan)->sum(fn($d) => $d['totalNegatif']);
$totalNetral = collect($dataBulanan)->sum(fn($d) => $d['totalNetral']);
$pdf = Pdf::loadView('laporan.tahunan', [
'tahun' => $tahun,
'judulLaporan' => "Laporan Analisis Sentimen Tahunan — $tahun",
'dataBulanan' => $dataBulanan,
'totalUlasan' => $totalUlasan,
'totalPositif' => $totalPositif,
'totalNegatif' => $totalNegatif,
'totalNetral' => $totalNetral,
'persen' => $totalUlasan > 0 ? [
'positif' => round(($totalPositif / $totalUlasan) * 100, 1),
'negatif' => round(($totalNegatif / $totalUlasan) * 100, 1),
'netral' => round(($totalNetral / $totalUlasan) * 100, 1),
] : ['positif' => 0, 'negatif' => 0, 'netral' => 0],
])
->setPaper('a4', 'portrait')
->setOptions(['dpi' => 110, 'defaultFont' => 'sans-serif', 'isHtml5ParserEnabled' => true]);
return $pdf->download("laporan-sentimen-$tahun.pdf");
}
// ─────────────────────────────────────────────
// Helper: kumpulkan semua data untuk satu periode
// ─────────────────────────────────────────────
private function collectData(int $periodeId): array
{
// Statistik sentimen
$totalUlasan = DB::table('hasil_analisis')->where('periode_id', $periodeId)->count();
$totalPositif = DB::table('hasil_analisis')->where('periode_id', $periodeId)->where('sentimen', 'positif')->count();
$totalNegatif = DB::table('hasil_analisis')->where('periode_id', $periodeId)->where('sentimen', 'negatif')->count();
$totalNetral = DB::table('hasil_analisis')->where('periode_id', $periodeId)->where('sentimen', 'netral')->count();
$persen = $totalUlasan > 0 ? [
'positif' => round(($totalPositif / $totalUlasan) * 100, 1),
'negatif' => round(($totalNegatif / $totalUlasan) * 100, 1),
'netral' => round(($totalNetral / $totalUlasan) * 100, 1),
] : ['positif' => 0, 'negatif' => 0, 'netral' => 0];
// Evaluasi model
$evaluasi = DB::table('evaluasi_model')
->where('periode_id', $periodeId)
->latest()
->first();
// Per destinasi
$perWisata = DB::table('hasil_analisis')
->where('periode_id', $periodeId)
->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")
)
->groupBy('wisata')
->get();
// Tabel hasil analisis (maks 200 baris agar PDF tidak terlalu besar)
$hasilAnalisis = DB::table('hasil_analisis')
->where('periode_id', $periodeId)
->select('wisata', 'ulasan_asli', 'sentimen', 'probabilitas')
->orderBy('wisata')
->limit(200)
->get();
// Rekomendasi (rule-based dari ulasan negatif)
$ulasanNegatif = DB::table('hasil_analisis')
->where('periode_id', $periodeId)
->where('sentimen', 'negatif')
->pluck('ulasan_bersih')
->implode(' ');
$rekomendasi = $this->generateRekomendasi($ulasanNegatif);
return compact(
'totalUlasan', 'totalPositif', 'totalNegatif', 'totalNetral',
'persen', 'evaluasi', 'perWisata', 'hasilAnalisis', 'rekomendasi'
);
}
// ─────────────────────────────────────────────
// Rule-based rekomendasi (sama seperti RekomendasiController)
// ─────────────────────────────────────────────
private function generateRekomendasi(string $text): array
{
$issueRules = [
'Kebersihan' => ['keywords' => ['kotor','sampah','jorok','kumuh','toilet','wc','bau'],
'icon' => '🧹', 'tip' => 'Tambah tempat sampah & jadwal pembersihan rutin.'],
'Aksesibilitas' => ['keywords' => ['parkir','parkiran','motor','mobil','jalan','macet'],
'icon' => '🚗', 'tip' => 'Sistem parkir lebih rapi dan transparan.'],
'Harga / Tiket' => ['keywords' => ['mahal','harga','tiket','bayar','biaya','tarif'],
'icon' => '🎟️', 'tip' => 'Penyesuaian harga tiket agar lebih terjangkau.'],
'Fasilitas' => ['keywords' => ['fasilitas','gazebo','warung','kantin','musholla','wifi','bangku'],
'icon' => '🏗️', 'tip' => 'Fasilitas lengkap & terawat meningkatkan kepuasan.'],
];
$result = [];
foreach ($issueRules as $nama => $rule) {
$skor = 0;
foreach ($rule['keywords'] as $kw) {
$skor += substr_count(strtolower($text), $kw);
}
if ($skor > 0) {
$result[] = ['nama' => $nama, 'skor' => $skor, 'icon' => $rule['icon'], 'tip' => $rule['tip']];
}
}
usort($result, fn($a, $b) => $b['skor'] - $a['skor']);
return array_slice($result, 0, 5);
}
}

View File

@ -12,62 +12,85 @@
class UlasanController extends Controller
{
public function index(Request $request)
{
$periodeList = DB::table('periode_analisis as p')
->whereExists(function ($q) {
$q->select(DB::raw(1))
->from('ulasan as u')
->whereColumn('u.periode_id', 'p.id');
})
->orWhereExists(function ($q) {
$q->select(DB::raw(1))
->from('hasil_analisis as h')
->whereColumn('h.periode_id', 'p.id');
})
->orderBy('p.tahun', 'desc')
->orderBy('p.bulan', 'desc')
// ->orderBy('p.id', 'desc')
->select('p.*')
->get();
{
$periodeList = DB::table('periode_analisis as p')
->whereExists(function ($q) {
$q->select(DB::raw(1))
->from('ulasan as u')
->whereColumn('u.periode_id', 'p.id');
})
->orWhereExists(function ($q) {
$q->select(DB::raw(1))
->from('hasil_analisis as h')
->whereColumn('h.periode_id', 'p.id');
})
->orderBy('p.tahun', 'desc')
->orderBy('p.bulan', 'desc')
->select('p.*')
->get();
$availablePeriodeIds = $periodeList->pluck('id')->map(fn($id) => (string) $id);
$requestedPeriodeId = $request->filled('periode_id') ? (string) $request->periode_id : null;
$latestFilledPeriodeId = $periodeList->sortByDesc('id')->first()->id ?? null;
$periodeId = $request->filled('periode_id')
&& $availablePeriodeIds->contains($requestedPeriodeId)
? $requestedPeriodeId
: ($latestFilledPeriodeId ?: optional($periodeList->first())->id);
$periodeAktif = $periodeId ? $periodeList->firstWhere('id', (int) $periodeId) : null;
$availablePeriodeIds = $periodeList->pluck('id')->map(fn($id) => (string) $id);
$query = Ulasan::query();
$requestedPeriodeId = $request->filled('periode_id')
? (string) $request->periode_id
: null;
// 🔍 SEARCH (ulasan + wisata)
if ($request->filled('search')) {
$search = $request->search;
// cari periode bulan & tahun sekarang
$currentMonth = now()->month;
$currentYear = now()->year;
$query->where(function ($q) use ($search) {
$q->where('ulasan', 'LIKE', "%$search%")
->orWhere('wisata', 'LIKE', "%$search%");
});
}
$currentPeriode = $periodeList->first(function ($periode) use ($currentMonth, $currentYear) {
return $periode->bulan == $currentMonth
&& $periode->tahun == $currentYear;
});
// 🔍 FILTER WISATA
if ($request->filled('wisata')) {
$query->where('wisata', 'LIKE', '%' . $request->wisata . '%');
}
// kalau bulan sekarang tidak ada → ambil periode terakhir
$defaultPeriodeId = $currentPeriode->id
?? $periodeList->sortByDesc('id')->first()->id
?? null;
// ← default/reset menampilkan periode terbaru
if ($periodeId) {
$query->where('periode_id', $periodeId);
}
// prioritas:
// 1. request manual dari dropdown
// 2. periode bulan sekarang
// 3. periode terakhir
$periodeId = $request->filled('periode_id')
&& $availablePeriodeIds->contains($requestedPeriodeId)
? $requestedPeriodeId
: $defaultPeriodeId;
$filteredQuery = clone $query;
$periodeAktif = $periodeId
? $periodeList->firstWhere('id', (int) $periodeId)
: null;
$totalUlasan = (clone $filteredQuery)->count();
$totalWisata = (clone $filteredQuery)->distinct('wisata')->count('wisata');
$lastUpdate = (clone $filteredQuery)->latest()->value('created_at');
$query = Ulasan::query();
$mentah = $query->orderBy('tanggal', 'desc')->paginate(20)->withQueryString();
// 🔍 SEARCH (ulasan + wisata)
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('ulasan', 'LIKE', "%$search%")
->orWhere('wisata', 'LIKE', "%$search%");
});
}
// 🔍 FILTER WISATA
if ($request->filled('wisata')) {
$query->where('wisata', 'LIKE', '%' . $request->wisata . '%');
}
// ← default/reset menampilkan periode terbaru
if ($periodeId) {
$query->where('periode_id', $periodeId);
}
$filteredQuery = clone $query;
$totalUlasan = (clone $filteredQuery)->count();
$totalWisata = (clone $filteredQuery)->distinct('wisata')->count('wisata');
$lastUpdate = (clone $filteredQuery)->latest()->value('created_at');
$mentah = $query->orderBy('tanggal', 'desc')->paginate(20)->withQueryString();
return view('ulasan.index', compact(
'mentah',
@ -77,11 +100,11 @@ public function index(Request $request)
'periodeList',
'periodeId',
'periodeAktif',
'latestFilledPeriodeId'
'defaultPeriodeId'
))->with([
'scraperDestinations' => $this->scraperDestinations(),
]);
}
}
public function riwayat()
{
@ -259,18 +282,6 @@ public function ambilData(Request $request)
->with('error', 'Scraping gagal: ' . $scraping['output']);
}
$periode = DB::table('periode_analisis')
->where('bulan', \Carbon\Carbon::parse($startDate)->month)
->where('tahun', \Carbon\Carbon::parse($startDate)->year)
->first();
$analisisArguments = $periode ? ['--periode-id', (string) $periode->id] : [];
$analisis = $this->runPythonScript(base_path('scraper/analisis.py'), 900, $analisisArguments);
if (!$analisis['success']) {
return redirect()->route('ulasan.index')
->with('error', 'Analisis gagal: ' . $analisis['output']);
}
$lokasiLabel = empty($validated['wisata']) ? 'semua lokasi' : $validated['wisata'];
$redirectParameters = [
@ -290,10 +301,10 @@ public function ambilData(Request $request)
$redirectParameters['tab'] = 'analisis';
}
return redirect()->route('dashboard', array_filter($redirectParameters))
->with('success', 'Data berhasil diambil dan dianalisis untuk ' . $lokasiLabel . ' periode ' . $periodeBulan->translatedFormat('F Y') . '.');
return redirect()->route('ulasan.index', array_filter($redirectParameters))
->with('success', 'Scraping berhasil dilakukan untuk ' . $lokasiLabel . ' periode ' . $periodeBulan->translatedFormat('F Y') . '. Silakan jalankan proses analisis.');
}
public function kosongkanPeriode(Request $request)
{
$periodeId = $request->validate([
@ -519,4 +530,4 @@ private function scraperDestinations(): array
'Kebun Teh Gunung Gambir',
];
}
}
}

View File

@ -101,23 +101,23 @@ public function destroy(int $id)
->with('success', 'Pengguna berhasil dihapus.');
}
public function store(Request $request)
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'role' => 'required|in:admin,staff',
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'role' => 'required|in:admin,staff',
'password' => 'required|min:8|confirmed',
]);
User::create([
'name' => $request->name,
'email' => $request->email,
'role' => $request->role,
'password' => bcrypt($request->password),
User::create([
'name' => $request->name,
'email' => $request->email,
'role' => $request->role,
'password' => Hash::make($request->password),
]);
return redirect()->route('kelola-pengguna.index')
->with('success', 'Pengguna berhasil ditambahkan.');
return redirect()->back()->with('success', 'Pengguna berhasil ditambahkan.');
}
}

View File

@ -7,6 +7,9 @@
class HasilAnalisis extends Model
{
protected $table = 'hasil_analisis';
protected static function booted(){
}
protected $fillable = [
'wisata',

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PeriodeAnalisis extends Model
{
protected $table = 'periode_analisis';
protected $fillable = [
'nama',
'bulan',
'tahun'
];
}

View File

@ -3,25 +3,16 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Paginator::useTailwind();
\Carbon\Carbon::setLocale('id');
//
}
}
}

View File

@ -10,6 +10,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^11.0",
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "^3.1"

524
composer.lock generated
View File

@ -4,8 +4,85 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ff902f4b41cdfe81df69eb5a9a27af85",
"content-hash": "855b99e0c7cb480cba79f70700deeb58",
"packages": [
{
"name": "barryvdh/laravel-dompdf",
"version": "v3.1.2",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-dompdf.git",
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^3.0",
"illuminate/support": "^9|^10|^11|^12|^13.0",
"php": "^8.1"
},
"require-dev": {
"larastan/larastan": "^2.7|^3.0",
"orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
"phpro/grumphp": "^2.5",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
},
"providers": [
"Barryvdh\\DomPDF\\ServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Barryvdh\\DomPDF\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "A DOMPDF Wrapper for Laravel",
"keywords": [
"dompdf",
"laravel",
"pdf"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2026-02-21T08:51:10+00:00"
},
{
"name": "brick/math",
"version": "0.14.8",
@ -533,6 +610,161 @@
],
"time": "2024-02-05T11:56:58+00:00"
},
{
"name": "dompdf/dompdf",
"version": "v3.1.5",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"shasum": ""
},
"require": {
"dompdf/php-font-lib": "^1.0.0",
"dompdf/php-svg-lib": "^1.0.0",
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-gd": "*",
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.5",
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"type": "library",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
},
"time": "2026-03-03T13:54:37+00:00"
},
{
"name": "dompdf/php-font-lib",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
},
"type": "library",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "The FontLib Community",
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
},
"time": "2026-01-20T14:10:26+00:00"
},
{
"name": "dompdf/php-svg-lib",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4 || ^9.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
},
"type": "library",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "The SvgLib Community",
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/dompdf/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
},
"time": "2026-01-02T16:01:13+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.6.0",
@ -2496,6 +2728,73 @@
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "masterminds/html5",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
},
"time": "2025-07-25T09:04:22+00:00"
},
{
"name": "monolog/monolog",
"version": "3.10.0",
@ -3879,6 +4178,86 @@
},
"time": "2025-12-14T04:43:48+00:00"
},
{
"name": "sabberworm/php-css-parser",
"version": "v9.3.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "1.4.0",
"phpstan/extension-installer": "1.4.3",
"phpstan/phpstan": "1.12.32 || 2.1.32",
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
"phpunit/phpunit": "8.5.52",
"rawr/phpunit-data-provider": "3.3.1",
"rector/rector": "1.2.10 || 2.2.8",
"rector/type-perfect": "1.0.0 || 2.1.0",
"squizlabs/php_codesniffer": "4.0.1",
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.4.x-dev"
}
},
"autoload": {
"files": [
"src/Rule/Rule.php",
"src/RuleSet/RuleContainer.php"
],
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Jake Hotson",
"email": "jake.github@qzdesign.co.uk"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0"
},
"time": "2026-03-03T17:31:43+00:00"
},
{
"name": "symfony/clock",
"version": "v7.4.8",
@ -6308,6 +6687,149 @@
],
"time": "2026-03-30T13:44:50+00:00"
},
{
"name": "thecodingmachine/safe",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/safe.git",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpstan/phpstan": "^2",
"phpunit/phpunit": "^10",
"squizlabs/php_codesniffer": "^3.2"
},
"type": "library",
"autoload": {
"files": [
"lib/special_cases.php",
"generated/apache.php",
"generated/apcu.php",
"generated/array.php",
"generated/bzip2.php",
"generated/calendar.php",
"generated/classobj.php",
"generated/com.php",
"generated/cubrid.php",
"generated/curl.php",
"generated/datetime.php",
"generated/dir.php",
"generated/eio.php",
"generated/errorfunc.php",
"generated/exec.php",
"generated/fileinfo.php",
"generated/filesystem.php",
"generated/filter.php",
"generated/fpm.php",
"generated/ftp.php",
"generated/funchand.php",
"generated/gettext.php",
"generated/gmp.php",
"generated/gnupg.php",
"generated/hash.php",
"generated/ibase.php",
"generated/ibmDb2.php",
"generated/iconv.php",
"generated/image.php",
"generated/imap.php",
"generated/info.php",
"generated/inotify.php",
"generated/json.php",
"generated/ldap.php",
"generated/libxml.php",
"generated/lzf.php",
"generated/mailparse.php",
"generated/mbstring.php",
"generated/misc.php",
"generated/mysql.php",
"generated/mysqli.php",
"generated/network.php",
"generated/oci8.php",
"generated/opcache.php",
"generated/openssl.php",
"generated/outcontrol.php",
"generated/pcntl.php",
"generated/pcre.php",
"generated/pgsql.php",
"generated/posix.php",
"generated/ps.php",
"generated/pspell.php",
"generated/readline.php",
"generated/rnp.php",
"generated/rpminfo.php",
"generated/rrd.php",
"generated/sem.php",
"generated/session.php",
"generated/shmop.php",
"generated/sockets.php",
"generated/sodium.php",
"generated/solr.php",
"generated/spl.php",
"generated/sqlsrv.php",
"generated/ssdeep.php",
"generated/ssh2.php",
"generated/stream.php",
"generated/strings.php",
"generated/swoole.php",
"generated/uodbc.php",
"generated/uopz.php",
"generated/url.php",
"generated/var.php",
"generated/xdiff.php",
"generated/xml.php",
"generated/xmlrpc.php",
"generated/yaml.php",
"generated/yaz.php",
"generated/zip.php",
"generated/zlib.php"
],
"classmap": [
"lib/DateTime.php",
"lib/DateTimeImmutable.php",
"lib/Exceptions/",
"generated/Exceptions/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": {
"issues": "https://github.com/thecodingmachine/safe/issues",
"source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
},
"funding": [
{
"url": "https://github.com/OskarStark",
"type": "github"
},
{
"url": "https://github.com/shish",
"type": "github"
},
{
"url": "https://github.com/silasjoisten",
"type": "github"
},
{
"url": "https://github.com/staabm",
"type": "github"
}
],
"time": "2026-02-04T18:08:13+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.4.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -9,7 +9,7 @@
$evaluasiTersedia = $evaluasi && $jumlahKelasAnalisis >= 2;
@endphp
<div class="space-y-6">
<div class="space-y-4">
{{-- ================= HEADER ================= --}}
<div class="flex justify-between items-center">
@ -62,30 +62,43 @@
{{-- ================= FILTER ================= --}}
<form id="filter-form" class="flex items-center gap-3">
<form id="filter-form" method="GET" action="{{ route('dashboard') }}" class="flex gap-2 mb-4">
<input type="hidden" name="tab" value="analisis">
@if(($periodeAktif->id ?? request('periode_id')))
<input type="hidden" name="periode_id" value="{{ $periodeAktif->id ?? request('periode_id') }}">
@endif
<input type="hidden" name="tab" value="analisis">
<select name="wisata" class="border rounded pl-3 pr-9 py-2 min-w-[220px]">
<option value="">Semua Destinasi</option>
@foreach(($wisataList ?? collect()) as $wisata)
<option value="{{ $wisata }}" {{ request('wisata') == $wisata ? 'selected' : '' }}>
{{ $wisata }}
</option>
@endforeach
</select>
<input type="hidden" name="periode_id" value="{{ $periodeAktif->id ?? '' }}">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
Filter
</button>
<select
name="wisata"
class="border rounded px-3 py-2"
>
</form>
<option value="">Semua Destinasi</option>
@foreach($wisataList as $item)
<option
value="{{ $item }}"
{{ request('wisata') == $item ? 'selected' : '' }}
>
{{ $item }}
</option>
@endforeach
</select>
<button
type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded"
>
Filter
</button>
</form>
{{-- ================= TABLE ================= --}}
<div class="bg-white rounded-xl shadow p-4">
<div class="bg-white rounded-xl shadow p-4 h-fit">
<h3 class="font-semibold mb-4">Detail Hasil Analisis Sentimen</h3>
@ -102,7 +115,7 @@
</thead>
<tbody>
@forelse($hasil as $item)
@forelse($hasil as $item)
<tr class="border-b">
<td>{{ ($hasil->currentPage() - 1) * $hasil->perPage() + $loop->iteration }}</td>
<td>{{ $item->wisata }}</td>

View File

@ -229,6 +229,11 @@ class="text-slate-400 hover:text-slate-700 text-2xl leading-none ml-4 flex-shrin
<form method="POST" id="editForm">
@csrf
@if ($errors->any())
<div class="mb-4 rounded-2xl bg-red-50 border border-red-200 text-red-600 px-4 py-3 text-sm">
{{ $errors->first() }}
</div>
@endif
@method('PUT')
<div class="mb-4">

View File

@ -0,0 +1,313 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: DejaVu Sans, sans-serif; font-size: 11px; color: #1f2937; background: #fff; }
.header-wrap { position: relative; background: #1a3a5c; color: white; margin-bottom: 0; overflow: hidden; }
.header-bg { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, #0f2744 0%, #1a4a7a 50%, #0d3060 100%); }
.header-content { position: relative; padding: 0; }
.header-top { display: flex; align-items: center; padding: 12px 20px 8px 20px; border-bottom: 1px solid rgba(255,255,255,0.15); }
.logo-circle { width: 48px; height: 48px; background: #c8a84b; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 12px; flex-shrink: 0; font-size: 14px; font-weight: bold; color: #1a3a5c; }
.logo-img { width: 48px; height: 48px; border-radius: 50%; object-fit: contain; margin-right: 12px; background: white; padding: 3px; }
.org1 { font-size: 11px; font-weight: bold; color: #c8a84b; letter-spacing: .5px; }
.org2 { font-size: 10px; color: rgba(255,255,255,.85); }
.header-body { padding: 14px 20px 16px 20px; }
.header-title { font-size: 24px; font-weight: bold; color: white; letter-spacing: 1px; text-transform: uppercase; line-height: 1.1; }
.header-sub { font-size: 14px; color: #c8a84b; font-weight: bold; letter-spacing: .5px; margin-bottom: 8px; text-transform: uppercase; }
.header-desc { font-size: 9.5px; color: rgba(255,255,255,.75); margin-bottom: 12px; line-height: 1.5; }
.periode-badge { display: inline-block; background: #1d4ed8; border-radius: 6px; padding: 7px 14px; }
.pb-label { font-size: 9px; color: rgba(255,255,255,.7); }
.pb-value { font-size: 13px; font-weight: bold; color: white; }
.page-body { padding: 16px 20px; }
.section { margin-bottom: 14px; }
.section-title { display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: bold; color: #1d4ed8; letter-spacing: .5px; text-transform: uppercase; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 2px solid #dbeafe; }
.stats { display: flex; gap: 8px; }
.stat-card { flex: 1; background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 10px 12px; display: flex; align-items: center; gap: 8px; }
.stat-icon { width: 34px; height: 34px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.stat-icon.blue { background: #eff6ff; } .stat-icon.green { background: #f0fdf4; } .stat-icon.red { background: #fef2f2; } .stat-icon.yellow { background: #fefce8; }
.slabel { font-size: 8.5px; color: #6b7280; }
.sval { font-size: 18px; font-weight: bold; line-height: 1.1; }
.spct { font-size: 8.5px; }
.sval.blue { color: #1d4ed8; } .sval.green { color: #16a34a; } .sval.red { color: #dc2626; } .sval.yellow { color: #d97706; }
.spct.green { color: #16a34a; } .spct.red { color: #dc2626; } .spct.yellow { color: #d97706; }
.two-col { display: flex; gap: 12px; }
.col-left { flex: 1; } .col-right { flex: 1; }
.donut-wrap { display: flex; align-items: center; gap: 16px; padding: 8px 0; }
.donut-chart { width: 100px; height: 100px; flex-shrink: 0; }
.donut-svg { width: 100px; height: 100px; transform: rotate(-90deg); }
.legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.legend-label { font-size: 9.5px; color: #374151; flex: 1; }
.legend-val { font-size: 9.5px; font-weight: bold; color: #111; }
table { width: 100%; border-collapse: collapse; }
.table-wrap { border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
thead tr { background: #1d4ed8; }
thead th { color: white; font-size: 9.5px; font-weight: bold; padding: 7px 10px; text-align: left; }
thead th.center { text-align: center; }
tbody tr:nth-child(even) { background: #f8fafc; }
tbody td { font-size: 9.5px; padding: 6px 10px; border-bottom: 1px solid #f1f5f9; color: #374151; }
tbody td.center { text-align: center; }
tfoot td { background: #eff6ff; font-weight: bold; color: #1d4ed8; font-size: 9.5px; padding: 6px 10px; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 8.5px; font-weight: bold; }
.badge-positif { background: #dcfce7; color: #15803d; }
.badge-negatif { background: #fee2e2; color: #b91c1c; }
.badge-netral { background: #fef9c3; color: #92400e; }
.rekom-item { display: flex; align-items: flex-start; gap: 10px; background: #f8fafc; border-radius: 8px; padding: 8px 10px; border: 1px solid #e5e7eb; margin-bottom: 6px; }
.rekom-icon { width: 28px; height: 28px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; }
.ri-red { background: #fee2e2; } .ri-orange { background: #ffedd5; } .ri-green { background: #dcfce7; } .ri-blue { background: #dbeafe; } .ri-purple { background: #ede9fe; }
.rtitle { font-weight: bold; font-size: 10px; color: #111827; margin-bottom: 2px; }
.rtip { font-size: 9px; color: #6b7280; line-height: 1.4; }
.eval-grid { display: flex; gap: 8px; }
.eval-box { flex: 1; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 9px 8px; text-align: center; }
.eval-label { font-size: 8.5px; color: #3b82f6; margin-bottom: 3px; }
.eval-val { font-size: 16px; font-weight: bold; color: #1d4ed8; }
.metodologi { background: #1a3a5c; color: white; padding: 14px 20px; display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; margin-top: 16px; }
.metod-left h4 { font-size: 10px; font-weight: bold; color: #c8a84b; margin-bottom: 5px; }
.metod-left p { font-size: 8.5px; color: rgba(255,255,255,.75); line-height: 1.5; }
.metod-right { text-align: right; flex-shrink: 0; }
.mdate { font-size: 9px; color: rgba(255,255,255,.7); }
.msys { font-size: 9.5px; font-weight: bold; color: #c8a84b; margin-top: 3px; }
.page-break { page-break-before: always; }
.no-break { page-break-inside: avoid; }
</style>
</head>
<body>
{{-- HEADER --}}
<div class="header-wrap">
<div class="header-bg"></div>
<div class="header-content">
<div class="header-top">
@php $logoPath = public_path('images/logo-sentara.png'); @endphp
@if(file_exists($logoPath))
<img src="{{ $logoPath }}" class="logo-img" alt="Logo">
@else
<div class="logo-circle">DISPAR</div>
@endif
<div>
<div class="org1">DINAS PARIWISATA</div>
<div class="org2">KABUPATEN JEMBER</div>
</div>
</div>
<div class="header-body">
<div class="header-title">LAPORAN ANALISIS SENTIMEN</div>
<div class="header-sub">Wisata Kabupaten Jember</div>
<div class="header-desc">Laporan ini berisi hasil analisis sentimen terhadap ulasan wisata dari Google Maps menggunakan metode Naive Bayes.</div>
<div class="periode-badge">
<div class="pb-label">Periode Analisis</div>
<div class="pb-value">{{ $periode->nama }}</div>
</div>
</div>
</div>
</div>
<div class="page-body">
{{-- RINGKASAN --}}
<div class="section no-break">
<div class="section-title">📊 RINGKASAN SENTIMEN</div>
<div class="stats">
<div class="stat-card">
<div class="stat-icon blue">💬</div>
<div><div class="slabel">Total Ulasan</div><div class="sval blue">{{ $totalUlasan }}</div><div class="spct" style="color:#6b7280">100% dari keseluruhan</div></div>
</div>
<div class="stat-card">
<div class="stat-icon green">😊</div>
<div><div class="slabel">Sentimen Positif</div><div class="sval green">{{ $totalPositif }}</div><div class="spct green">{{ $persen['positif'] }}%</div></div>
</div>
<div class="stat-card">
<div class="stat-icon red">😟</div>
<div><div class="slabel">Sentimen Negatif</div><div class="sval red">{{ $totalNegatif }}</div><div class="spct red">{{ $persen['negatif'] }}%</div></div>
</div>
<div class="stat-card">
<div class="stat-icon yellow">😐</div>
<div><div class="slabel">Sentimen Netral</div><div class="sval yellow">{{ $totalNetral }}</div><div class="spct yellow">{{ $persen['netral'] }}%</div></div>
</div>
</div>
</div>
{{-- DISTRIBUSI + EVALUASI --}}
<div class="two-col no-break">
<div class="col-left">
<div class="section-title">🍩 DISTRIBUSI SENTIMEN</div>
@php
$t = max($totalUlasan, 1);
$r = 38; $circ = 2 * 3.14159 * $r;
$pP = $totalPositif / $t;
$pN = $totalNegatif / $t;
$pNt = $totalNetral / $t;
@endphp
<div class="donut-wrap">
<div class="donut-chart">
<svg class="donut-svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="{{ $r }}" fill="none" stroke="#e5e7eb" stroke-width="18"/>
@if($pP > 0)<circle cx="50" cy="50" r="{{ $r }}" fill="none" stroke="#22c55e" stroke-width="18" stroke-dasharray="{{ $circ*$pP }} {{ $circ*(1-$pP) }}" stroke-dashoffset="0"/>@endif
@if($pN > 0)<circle cx="50" cy="50" r="{{ $r }}" fill="none" stroke="#ef4444" stroke-width="18" stroke-dasharray="{{ $circ*$pN }} {{ $circ*(1-$pN) }}" stroke-dashoffset="-{{ $circ*$pP }}"/>@endif
@if($pNt > 0)<circle cx="50" cy="50" r="{{ $r }}" fill="none" stroke="#f59e0b" stroke-width="18" stroke-dasharray="{{ $circ*$pNt }} {{ $circ*(1-$pNt) }}" stroke-dashoffset="-{{ $circ*($pP+$pN) }}"/>@endif
<circle cx="50" cy="50" r="22" fill="white"/>
</svg>
</div>
<div>
<div class="legend-item"><div class="legend-dot" style="background:#22c55e"></div><span class="legend-label">Positif</span><span class="legend-val">{{ $persen['positif'] }}% ({{ $totalPositif }})</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div><span class="legend-label">Negatif</span><span class="legend-val">{{ $persen['negatif'] }}% ({{ $totalNegatif }})</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div><span class="legend-label">Netral</span><span class="legend-val">{{ $persen['netral'] }}% ({{ $totalNetral }})</span></div>
</div>
</div>
</div>
<div class="col-right">
<div class="section-title">🎯 EVALUASI MODEL</div>
@if($evaluasi)
<div class="eval-grid">
<div class="eval-box"><div class="eval-label">Precision</div><div class="eval-val">{{ number_format($evaluasi->precision,2) }}</div></div>
<div class="eval-box"><div class="eval-label">Recall</div><div class="eval-val">{{ number_format($evaluasi->recall,2) }}</div></div>
<div class="eval-box"><div class="eval-label">F1 Score</div><div class="eval-val">{{ number_format($evaluasi->f1_score,2) }}</div></div>
<div class="eval-box"><div class="eval-label">Akurasi</div><div class="eval-val">{{ number_format($evaluasi->accuracy,2) }}</div></div>
</div>
@else
<p style="font-size:10px;color:#9ca3af;padding:20px 0">Data evaluasi belum tersedia.</p>
@endif
</div>
</div>
{{-- TABEL PER DESTINASI --}}
<div class="two-col no-break" style="margin-top:14px">
<div class="col-left">
<div class="section-title">🔭 TOTAL DATA PER DESTINASI</div>
<div class="table-wrap">
<table>
<thead><tr><th>No</th><th>Destinasi Wisata</th><th class="center">Total Ulasan</th></tr></thead>
<tbody>
@foreach($perWisata as $i => $w)
<tr><td>{{ $i+1 }}</td><td style="font-weight:bold">{{ $w->wisata }}</td><td class="center">{{ $w->total }}</td></tr>
@endforeach
</tbody>
<tfoot><tr><td colspan="2" style="font-weight:bold;padding:6px 10px">Total</td><td class="center" style="padding:6px 10px;font-weight:bold">{{ $totalUlasan }}</td></tr></tfoot>
</table>
</div>
</div>
<div class="col-right">
<div class="section-title">📉 SENTIMEN PER DESTINASI</div>
<div class="table-wrap">
<table>
<thead><tr><th>No</th><th>Destinasi Wisata</th><th class="center" style="color:#86efac">Positif</th><th class="center" style="color:#fca5a5">Negatif</th><th class="center" style="color:#fde68a">Netral</th></tr></thead>
<tbody>
@foreach($perWisata as $i => $w)
<tr>
<td>{{ $i+1 }}</td><td style="font-weight:bold">{{ $w->wisata }}</td>
<td class="center" style="color:#16a34a;font-weight:bold">{{ $w->positif }}</td>
<td class="center" style="color:#dc2626;font-weight:bold">{{ $w->negatif }}</td>
<td class="center" style="color:#d97706;font-weight:bold">{{ $w->netral }}</td>
</tr>
@endforeach
</tbody>
<tfoot><tr>
<td colspan="2" style="padding:6px 10px;font-weight:bold">Total</td>
<td class="center" style="padding:6px 10px;color:#16a34a;font-weight:bold">{{ $totalPositif }}</td>
<td class="center" style="padding:6px 10px;color:#dc2626;font-weight:bold">{{ $totalNegatif }}</td>
<td class="center" style="padding:6px 10px;color:#d97706;font-weight:bold">{{ $totalNetral }}</td>
</tr></tfoot>
</table>
</div>
</div>
</div>
{{-- DETAIL + REKOMENDASI --}}
<div class="two-col no-break" style="margin-top:14px">
<div class="col-left">
<div class="section-title">📋 DETAIL HASIL ANALISIS</div>
<div class="table-wrap">
<table>
<thead><tr><th width="6%">No</th><th width="22%">Destinasi</th><th width="44%">Ulasan</th><th width="16%">Sentimen</th><th width="12%" class="center">Prob.</th></tr></thead>
<tbody>
@foreach($hasilAnalisis->take(8) as $i => $item)
<tr>
<td>{{ $i+1 }}</td>
<td style="font-size:8.5px">{{ $item->wisata }}</td>
<td style="font-size:8.5px">{{ \Illuminate\Support\Str::limit($item->ulasan_asli ?? '-', 75) }}</td>
<td><span class="badge badge-{{ strtolower($item->sentimen) }}">{{ ucfirst($item->sentimen) }}</span></td>
<td class="center">{{ number_format($item->probabilitas,2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if($hasilAnalisis->count() > 8)
<p style="font-size:8.5px;color:#3b82f6;margin-top:5px;text-align:right">* Lihat semua data ulasan pada sistem </p>
@endif
</div>
<div class="col-right">
<div class="section-title">💡 REKOMENDASI LAYANAN</div>
@php $iconColors = ['red','orange','green','blue','purple']; @endphp
@forelse($rekomendasi as $i => $r)
@php $c = $iconColors[$i % count($iconColors)]; @endphp
<div class="rekom-item">
<div class="rekom-icon ri-{{ $c }}">{{ $r['icon'] }}</div>
<div><div class="rtitle">{{ $r['nama'] }}</div><div class="rtip">{{ $r['tip'] }}</div></div>
</div>
@empty
<p style="font-size:10px;color:#9ca3af;padding:20px 0;text-align:center">Tidak ada keluhan signifikan. 🎉</p>
@endforelse
</div>
</div>
</div>
{{-- FOOTER --}}
<div class="metodologi">
<div class="metod-left">
<h4>METODOLOGI</h4>
<p>Analisis sentimen dilakukan menggunakan metode Naive Bayes Classifier pada data ulasan Google Maps yang telah melalui proses preprocessing (teks cleaning, case folding, tokenizing, filtering, stemming).</p>
</div>
<div class="metod-right">
<div class="mdate">Dicetak pada: {{ now()->translatedFormat('d F Y H:i') }} WIB</div>
<div class="msys">Sistem Analisis Sentimen Wisata</div>
<div style="font-size:8.5px;color:rgba(255,255,255,.6)">Kabupaten Jember</div>
</div>
</div>
{{-- HALAMAN 2: TABEL LENGKAP --}}
@if($hasilAnalisis->count() > 8)
<div class="page-break"></div>
<div class="page-body">
<div style="margin-bottom:12px">
<div style="font-size:14px;font-weight:bold;color:#1d4ed8">Detail Lengkap Hasil Analisis</div>
<div style="font-size:9px;color:#6b7280">Periode: {{ $periode->nama }} Total {{ $hasilAnalisis->count() }} data</div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th width="5%">No</th><th width="20%">Destinasi</th><th width="55%">Ulasan Asli</th><th width="12%">Sentimen</th><th width="8%" class="center">Prob.</th></tr></thead>
<tbody>
@foreach($hasilAnalisis as $i => $item)
<tr>
<td>{{ $i+1 }}</td>
<td style="font-size:8.5px">{{ $item->wisata }}</td>
<td style="font-size:8.5px">{{ \Illuminate\Support\Str::limit($item->ulasan_asli ?? '-', 130) }}</td>
<td><span class="badge badge-{{ strtolower($item->sentimen) }}">{{ ucfirst($item->sentimen) }}</span></td>
<td class="center">{{ number_format($item->probabilitas,2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="metodologi" style="margin-top:16px">
<div class="metod-left"><h4>METODOLOGI</h4><p>Analisis sentimen menggunakan Naive Bayes Classifier dengan preprocessing teks.</p></div>
<div class="metod-right"><div class="mdate">{{ now()->translatedFormat('d F Y H:i') }} WIB</div><div class="msys">SENTARA Sistem Analisis Wisata Jember</div></div>
</div>
</div>
@endif
</body>
</html>

View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>
Laporan Tahunan
</title>
<style>
body{
font-family: sans-serif;
font-size: 12px;
color: #222;
}
h1,h2,h3{
margin-bottom: 5px;
}
.title{
text-align: center;
margin-bottom: 30px;
}
table{
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
table th{
background: #f3f3f3;
}
table th,
table td{
border: 1px solid #ccc;
padding: 8px;
}
</style>
</head>
<body>
<div class="title">
<h1>
LAPORAN TAHUNAN ANALISIS SENTIMEN
</h1>
<h2>
Wisata Kabupaten Jember
</h2>
<p>
Tahun:
<strong>
{{ $tahun }}
</strong>
</p>
</div>
<table>
<tr>
<th>Total Ulasan</th>
<td>{{ $totalUlasan ?? 0 }}</td>
</tr>
<tr>
<th>Positif</th>
<td>{{ $positif ?? 0 }}</td>
</tr>
<tr>
<th>Negatif</th>
<td>{{ $negatif ?? 0 }}</td>
</tr>
<tr>
<th>Netral</th>
<td>{{ $netral ?? 0 }}</td>
</tr>
</table>
<br>
<h3>Rekomendasi Tahunan</h3>
@forelse(($rekomendasi ?? []) as $item)
<p>
{{ $item->rekomendasi }}
</p>
@empty
<p>
Tidak ada rekomendasi.
</p>
@endforelse
</body>
</html>

View File

@ -215,4 +215,4 @@ class="border rounded-lg pl-3 pr-9 py-2 text-sm min-w-[220px] focus:outline-none
</div>
</div>
</div>

View File

@ -12,10 +12,7 @@
</p>
</div>
<a href="{{ route('ulasan.index') }}"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm shadow text-center">
Ambil Data Baru
</a>
</div>
@if(session('success'))
@ -81,10 +78,7 @@ class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-0" required>
</td>
<td class="px-4 py-3">
<div class="flex flex-wrap justify-center gap-2">
<a href="{{ route('dashboard', ['periode_id' => $item->id]) }}"
class="px-3 py-1 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100">
Dashboard
</a>
<a href="{{ route('dashboard', ['periode_id' => $item->id, 'tab' => 'analisis']) }}"
class="px-3 py-1 rounded-lg bg-green-50 text-green-600 hover:bg-green-100">
Analisis
@ -116,4 +110,4 @@ class="px-3 py-1 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200">
</div>
@endsection
@endsection

View File

@ -9,6 +9,7 @@
use App\Http\Controllers\UserController;
/*
|--------------------------------------------------------------------------
| ROUTE AWAL

View File

@ -86,7 +86,7 @@ def db_config():
"connection": connection,
"host": env_value(env, "DB_HOST", "127.0.0.1"),
"port": int(env_value(env, "DB_PORT", "3306")),
"database": env_value(env, "DB_DATABASE", "sentara"),
"database": env_value(env, "DB_DATABASE", "sistem_analisis"),
"user": env_value(env, "DB_USERNAME", "root"),
"password": env_value(env, "DB_PASSWORD", ""),
}
@ -158,6 +158,7 @@ POSITIF_WORDS = {
"murah", "asyik", "ramah", "worth", "spektakuler", "memukau",
"sejuk", "kece", "amazing", "beautiful", "good", "nice", "best",
"great", "perfect", "recommend", "memuaskan", "menyenangkan", "view",
"seru", "enak", "adem",
}
NEGATIF_WORDS = {
@ -195,27 +196,45 @@ def preprocess_text(text):
return stemmer.stem(" ".join(words)).strip()
# def label_by_keyword(clean_text):
# words = set(clean_text.split())
# positive_score = len(words & POSITIF_WORDS)
# negative_score = len(words & NEGATIF_WORDS)
# if positive_score > negative_score:
# return "positif"
# if negative_score > positive_score:
# return "negatif"
# return "netral"
def label_by_keyword(clean_text):
words = set(clean_text.split())
positive_score = len(words & POSITIF_WORDS)
negative_score = len(words & NEGATIF_WORDS)
words = clean_text.split()
positive_score = sum(1 for word in words if word in POSITIF_WORDS)
negative_score = sum(1 for word in words if word in NEGATIF_WORDS)
if positive_score > negative_score:
return "positif"
if negative_score > positive_score:
elif negative_score > positive_score:
return "negatif"
return "netral"
def make_pseudo_label(row):
rating = normalize_rating(row.get("rating"))
if rating is not None:
if rating >= 4:
return "positif"
if rating == 3:
return "netral"
return "negatif"
# def make_pseudo_label(row):
# rating = normalize_rating(row.get("rating"))
# if rating is not None:
# if rating >= 4:
# return "positif"
# if rating == 3:
# return "netral"
# return "negatif"
# return label_by_keyword(row["ulasan_bersih"])
def make_pseudo_label(row):
return label_by_keyword(row["ulasan_bersih"])
@ -231,10 +250,20 @@ def rating_confidence(value):
def apply_rating_priority(row, model_classes=None):
rating_label = make_pseudo_label(row)
rating = normalize_rating(row.get("rating"))
if rating is None:
return row["sentimen"], float(row["probabilitas"])
sentiment_result = row["sentimen"]
probability = float(row["probabilitas"])
# Jika probabilitas model sangat rendah (di bawah 0.5), baru gunakan rating
if probability < 0.5:
rating_label = make_pseudo_label(row)
return rating_label, 0.5
return sentiment_result, probability
# rating_label = make_pseudo_label(row)
# rating = normalize_rating(row.get("rating"))
# if rating is None:
# return row["sentimen"], float(row["probabilitas"])
if row["sentimen"] != rating_label:
log(
@ -288,9 +317,10 @@ def main():
df = pd.read_sql(
prepare_sql(
raw_conn,
"SELECT id, wisata, reviewer, rating, ulasan, tanggal, periode_id "
"FROM ulasan WHERE periode_id = %s",
"SELECT u.id, u.wisata, u.reviewer, u.rating, u.ulasan, u.tanggal, u.periode_id "
"FROM ulasan u WHERE u.periode_id = %s AND NOT EXISTS ( SELECT 1 FROM hasil_analisis h WHERE h.ulasan_id = u.id )"
),
engine,
params=(periode_id,),
)
@ -302,6 +332,8 @@ def main():
df["ulasan"] = df["ulasan"].astype(str)
df = df[df["ulasan"].str.strip().ne("")]
df = df[df["ulasan"].str.strip().ne("0")]
df = df[~df["ulasan"].str.contains(r"\[Tanpa teks\]", na=False)]
df = df[df["ulasan"].str.len() > 5]
if df.empty:
fail("Data ulasan kosong setelah validasi teks.")
@ -359,6 +391,7 @@ def main():
df["sentimen"] = model.predict(X_all_vec)
probability_matrix = model.predict_proba(X_all_vec)
df["probabilitas"] = probability_matrix.max(axis=1)
df["probabilitas"] = df["probabilitas"].clip(upper=0.99)
df["probabilitas_by_class"] = list(probability_matrix)
final_results = df.apply(lambda row: apply_rating_priority(row, model.classes_), axis=1)
df["sentimen"] = [result[0] for result in final_results]
@ -370,11 +403,12 @@ def main():
log("INFO", "Evaluasi memakai pseudo-label dari rating/rule otomatis, bukan label manual.")
execute(cursor, raw_conn, "DELETE FROM hasil_analisis WHERE periode_id = %s", (periode_id,))
execute(cursor, raw_conn, "DELETE FROM evaluasi_model WHERE periode_id = %s", (periode_id,))
# execute(cursor, raw_conn, "DELETE FROM hasil_analisis WHERE periode_id = %s", (periode_id,))
# execute(cursor, raw_conn, "DELETE FROM evaluasi_model WHERE periode_id = %s", (periode_id,))
hasil_columns = table_columns(cursor, raw_conn, "hasil_analisis")
insert_columns = [
"ulasan_id",
"wisata",
"ulasan_asli",
"ulasan_bersih",
@ -396,6 +430,7 @@ def main():
for _, row in df.fillna("").iterrows():
values = [
int(row["id"]),
str(row["wisata"]),
str(row["ulasan"]),
str(row["ulasan_bersih"]),

329
scraper/analisis_baru.py Normal file
View File

@ -0,0 +1,329 @@
# -*- coding: utf-8 -*-
import re
import sys
import pickle
from pathlib import Path
from urllib.parse import quote_plus
import pandas as pd
import pymysql
from sqlalchemy import create_engine
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
import nltk
from nltk.corpus import stopwords
# =========================
# UTF-8
# =========================
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
# =========================
# NLTK
# =========================
nltk.download('stopwords')
# =========================
# BASE DIRECTORY
# =========================
BASE_DIR = Path(__file__).resolve().parents[1]
# =========================
# LOAD MODEL
# =========================
with open('model/model_sentiment.pkl', 'rb') as file:
model = pickle.load(file)
with open('model/tfidf_vectorizer.pkl', 'rb') as file:
tfidf = pickle.load(file)
# =========================
# PREPROCESSING
# =========================
stop_words = set(stopwords.words('indonesian'))
factory = StemmerFactory()
stemmer = factory.create_stemmer()
def preprocess_text(text):
# handle null
if text is None:
return ''
# ubah string
text = str(text)
# lowercase
text = text.lower()
# hapus angka
text = re.sub(r'\d+', '', text)
# hapus simbol
text = re.sub(r'[^\w\s]', '', text)
# hapus spasi berlebih
text = re.sub(r'\s+', ' ', text).strip()
# tokenizing
words = text.split()
# stopword removal
words = [word for word in words if word not in stop_words]
# stemming
words = [stemmer.stem(word) for word in words]
return ' '.join(words)
# =========================
# READ LARAVEL .ENV
# =========================
def read_laravel_env():
env = {}
env_file = BASE_DIR / '.env'
if not env_file.exists():
return env
for line in env_file.read_text(encoding='utf-8').splitlines():
line = line.strip()
if not line:
continue
if line.startswith('#'):
continue
if '=' not in line:
continue
key, value = line.split('=', 1)
env[key.strip()] = value.strip()
return env
env = read_laravel_env()
DB_HOST = env.get('DB_HOST', '127.0.0.1')
DB_PORT = env.get('DB_PORT', '3306')
DB_DATABASE = env.get('DB_DATABASE', 'sistem_analisis')
DB_USERNAME = env.get('DB_USERNAME', 'root')
DB_PASSWORD = env.get('DB_PASSWORD', '')
# =========================
# DATABASE CONNECTION
# =========================
engine_url = (
"mysql+pymysql://"
f"{quote_plus(DB_USERNAME)}:{quote_plus(DB_PASSWORD)}"
f"@{DB_HOST}:{DB_PORT}/{DB_DATABASE}?charset=utf8mb4"
)
engine = create_engine(engine_url)
# =========================
# VALIDASI ARGUMENT
# =========================
if len(sys.argv) < 2:
print("periode_id tidak ditemukan")
sys.exit()
periode_id = sys.argv[1]
# =========================
# AMBIL DATA ULASAN
# =========================
query = f"""
SELECT
id,
wisata,
ulasan,
rating,
tanggal,
periode_id
FROM ulasan
WHERE periode_id = {periode_id}
AND id NOT IN (
SELECT ulasan_id
FROM hasil_analisis
)
"""
df = pd.read_sql(query, engine)
# =========================
# VALIDASI DATA
# =========================
if df.empty:
print("Tidak ada data baru untuk dianalisis.")
sys.exit()
# =========================
# FILTER DATA KOTOR
# =========================
clean_rows = []
for _, row in df.iterrows():
ulasan = str(row['ulasan']).strip()
# skip kosong
if ulasan == '':
continue
# skip null text
if ulasan.lower() == '[tanpa teks]':
continue
# skip pendek
if len(ulasan) < 5:
continue
# skip angka semua
if ulasan.isdigit():
continue
clean_rows.append(row)
# =========================
# DATAFRAME BERSIH
# =========================
if len(clean_rows) == 0:
print("Tidak ada data bersih untuk dianalisis.")
sys.exit()
df = pd.DataFrame(clean_rows)
# =========================
# HAPUS DUPLIKAT
# =========================
df = df.drop_duplicates(subset=['ulasan'])
# =========================
# PREPROCESSING
# =========================
df['ulasan_bersih'] = df['ulasan'].apply(preprocess_text)
# =========================
# HAPUS YANG JADI KOSONG
# =========================
df = df[df['ulasan_bersih'].str.strip() != '']
# =========================
# VALIDASI ULANG
# =========================
if df.empty:
print("Tidak ada data valid setelah preprocessing.")
sys.exit()
# =========================
# TF-IDF
# =========================
X = tfidf.transform(df['ulasan_bersih'])
# =========================
# PREDIKSI SENTIMEN
# =========================
df['sentimen'] = model.predict(X)
probability_matrix = model.predict_proba(X)
df['probabilitas'] = probability_matrix.max(axis=1)
# =========================
# SIMPAN KE DATABASE
# =========================
conn = engine.raw_connection()
cursor = conn.cursor()
jumlah_berhasil = 0
for _, row in df.iterrows():
try:
sql = """
INSERT INTO hasil_analisis
(
ulasan_id,
wisata,
ulasan_asli,
ulasan_terolah,
ulasan_bersih,
hasil_preprocessing,
sentimen,
probabilitas,
periode_id,
created_at,
updated_at
)
VALUES
(
%s,%s,%s,%s,%s,%s,%s,%s,%s,NOW(),NOW()
)
"""
values = (
int(row['id']),
str(row['wisata']),
str(row['ulasan']),
str(row['ulasan_bersih']),
str(row['ulasan_bersih']),
str(row['ulasan_bersih']),
str(row['sentimen']),
float(row['probabilitas']),
int(row['periode_id'])
)
cursor.execute(sql, values)
jumlah_berhasil += 1
except Exception as e:
print(f"Gagal insert data ID {row['id']} : {e}")
conn.commit()
cursor.close()
conn.close()
# =========================
# OUTPUT
# =========================
print(f"{jumlah_berhasil} data berhasil dianalisis!")

295
scraper/app.py Normal file
View File

@ -0,0 +1,295 @@
from flask import Flask, request, jsonify
import pickle
import re
import subprocess
import nltk
from nltk.corpus import stopwords
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
# =========================
# LOAD MODEL
# =========================
with open('model/model_sentiment.pkl', 'rb') as file:
model = pickle.load(file)
with open('model/tfidf_vectorizer.pkl', 'rb') as file:
tfidf = pickle.load(file)
# =========================
# PREPROCESSING
# =========================
nltk.download('stopwords')
stop_words = set(stopwords.words('indonesian'))
factory = StemmerFactory()
stemmer = factory.create_stemmer()
def preprocess_text(text):
text = str(text).lower()
# normalisasi huruf berulang
text = re.sub(r'(.)\1+', r'\1', text)
# hapus angka
text = re.sub(r'\d+', '', text)
# hapus simbol
text = re.sub(r'[^\w\s]', '', text)
# hapus spasi berlebih
text = re.sub(r'\s+', ' ', text).strip()
# tokenizing
words = text.split()
# slang normalization
slang_dict = {
'gk': 'tidak',
'ga': 'tidak',
'nggak': 'tidak',
'bgt': 'banget',
'bgtt': 'banget',
'bangett': 'banget',
'bgus': 'bagus',
'baguss': 'bagus',
'tmpt': 'tempat',
'jln': 'jalan',
'sy': 'saya',
'yg': 'yang',
'udh': 'sudah'
}
words = [
slang_dict[word] if word in slang_dict else word
for word in words
]
# stopword removal
words = [
word for word in words
if word not in stop_words
]
# stemming
words = [stemmer.stem(word) for word in words]
return ' '.join(words)
# lowercase
text = text.lower()
# hapus angka
text = re.sub(r'\d+', '', text)
# hapus simbol
text = re.sub(r'[^\w\s]', '', text)
# hapus spasi berlebih
text = re.sub(r'\s+', ' ', text).strip()
# tokenizing
words = text.split()
# stopword removal
words = [word for word in words if word not in stop_words]
# stemming
words = [stemmer.stem(word) for word in words]
return ' '.join(words)
# lowercase
text = text.lower()
# hapus angka
text = re.sub(r'\d+', '', text)
# hapus simbol
text = re.sub(r'[^\w\s]', '', text)
# hapus spasi berlebih
text = re.sub(r'\s+', ' ', text).strip()
# tokenizing
words = text.split()
# stopword removal
words = [word for word in words if word not in stop_words]
# stemming
words = [stemmer.stem(word) for word in words]
return ' '.join(words)
# =========================
# FLASK APP
# =========================
app = Flask(__name__)
@app.route('/scraping', methods=['POST'])
def scraping():
try:
data = request.get_json()
wisata = data.get('wisata', '')
result = subprocess.run(
['python', 'scraping_pipeline.py', wisata],
capture_output=True,
text=True
)
if result.returncode != 0:
return jsonify({
'status': 'error',
'error': result.stderr
}), 500
return jsonify({
'status': 'success',
'message': result.stdout
})
except Exception as e:
return jsonify({
'status': 'error',
'error': str(e)
}), 500
try:
result = subprocess.run(
['python', 'scraping_pipeline.py'],
capture_output=True,
text=True
)
if result.returncode != 0:
return jsonify({
'status': 'error',
'error': result.stderr
}), 500
return jsonify({
'status': 'success',
'message': result.stdout
})
except Exception as e:
return jsonify({
'status': 'error',
'error': str(e)
}), 500
# ======================================
# PREDICT 1 REVIEW
# ======================================
@app.route('/predict', methods=['POST'])
def predict():
data = request.get_json()
review = data['review']
# preprocessing
clean_review = preprocess_text(review)
# tfidf transform
vector = tfidf.transform([clean_review])
# prediksi
prediction = model.predict(vector)[0]
# probabilitas
probability = model.predict_proba(vector).max()
return jsonify({
'review': review,
'clean_review': clean_review,
'sentiment': prediction,
'probability': round(float(probability), 4)
})
# ======================================
# PROSES ANALISIS SEMUA DATA
# ======================================
@app.route('/proses-analisis', methods=['POST'])
def proses_analisis():
try:
data = request.get_json()
periode_id = str(data.get('periode_id'))
result = subprocess.run(
['python', 'analisis.py', periode_id],
capture_output=True,
text=True
)
if result.returncode != 0:
return jsonify({
'status': 'error',
'error': result.stderr
}), 500
return jsonify({
'status': 'success',
'message': result.stdout
})
except Exception as e:
return jsonify({
'status': 'error',
'error': str(e)
}), 500
try:
result = subprocess.run(
['python', 'analisis.py'],
capture_output=True,
text=True
)
if result.returncode != 0:
return jsonify({
'status': 'error',
'error': result.stderr
}), 500
return jsonify({
'status': 'success',
'message': result.stdout
})
except Exception as e:
return jsonify({
'status': 'error',
'error': str(e)
}), 500
# =========================
# RUN APP
# =========================
if __name__ == '__main__':
app.run(debug=False)

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -114,7 +114,7 @@ def db_config():
"connection": connection,
"host": env_value(env, "DB_HOST", "127.0.0.1"),
"port": int(env_value(env, "DB_PORT", "3306")),
"database": env_value(env, "DB_DATABASE", "analisis_sentimen"),
"database": env_value(env, "DB_DATABASE", "sistem_analisis"),
"user": env_value(env, "DB_USERNAME", "root"),
"password": env_value(env, "DB_PASSWORD", ""),
}
@ -768,7 +768,6 @@ def find_reviews_scroll_container(driver, wait):
def scrape_with_selenium(cursor, conn, periode_id, start_date, end_date, config, destinations):
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

151
scraper/train_model.py Normal file
View File

@ -0,0 +1,151 @@
import pandas as pd
import re
import nltk
import pickle
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
from nltk.corpus import stopwords
# Download stopword nltk
nltk.download('stopwords')
# =========================
# LOAD DATASET
# =========================
df = pd.read_excel('dataset/Labeled_LawangSewu_ReviewData.xlsx')
# Ambil kolom penting
df = df[['Text', 'Label']]
# Rename kolom
df.columns = ['review', 'label']
# Lowercase label
df['label'] = df['label'].str.lower()
# Hapus label tidak relevan
df = df[df['label'] != 'tidak relevan']
# Hapus data kosong
df = df.dropna()
# Hapus duplicate
df = df.drop_duplicates(subset=['review'])
# =========================
# PREPROCESSING
# =========================
# Stopword Indonesia
stop_words = set(stopwords.words('indonesian'))
# Stemmer Indonesia
factory = StemmerFactory()
stemmer = factory.create_stemmer()
def preprocess_text(text):
# lowercase
text = text.lower()
# hapus angka
text = re.sub(r'\d+', '', text)
# hapus simbol
text = re.sub(r'[^\w\s]', '', text)
# hapus spasi berlebih
text = re.sub(r'\s+', ' ', text).strip()
# tokenizing
words = text.split()
# stopword removal
words = [word for word in words if word not in stop_words]
# stemming
words = [stemmer.stem(word) for word in words]
return ' '.join(words)
# Terapkan preprocessing
df['clean_review'] = df['review'].apply(preprocess_text)
# =========================
# HASIL
# =========================
print(df[['review', 'clean_review', 'label']].head())
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import classification_report, accuracy_score
# =========================
# TF-IDF
# =========================
X = df['clean_review']
y = df['label']
tfidf = TfidfVectorizer(
max_features=5000,
ngram_range=(1,2),
min_df=3,
max_df=0.9,
sublinear_tf=True
)
X_tfidf = tfidf.fit_transform(X)
# =========================
# SPLIT DATA
# =========================
X_train, X_test, y_train, y_test = train_test_split(
X_tfidf,
y,
test_size=0.2,
random_state=42,
stratify=y
)
# =========================
# TRAIN MODEL
# =========================
model = MultinomialNB(fit_prior=False)
model.fit(X_train, y_train)
# =========================
# PREDIKSI
# =========================
y_pred = model.predict(X_test)
# =========================
# EVALUASI
# =========================
accuracy = accuracy_score(y_test, y_pred)
print("\n=== HASIL EVALUASI MODEL ===")
print(f"Akurasi: {accuracy:.4f}")
print("\nClassification Report:")
print(classification_report(y_test, y_pred))
# =========================
# SIMPAN MODEL
# =========================
with open('model/model_sentiment.pkl', 'wb') as file:
pickle.dump(model, file)
with open('model/tfidf_vectorizer.pkl', 'wb') as file:
pickle.dump(tfidf, file)
print("\nModel berhasil disimpan!")

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
<?php echo e($attributes); ?>
>
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/icons/moon.blade.php ENDPATH**/ ?>

After

Width:  |  Height:  |  Size: 609 B

View File

@ -0,0 +1,51 @@
<?php if (isset($component)) { $__componentOriginal74daf2d0a9c625ad90327a6043d15980 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal74daf2d0a9c625ad90327a6043d15980 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.card','data' => []] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::card'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes([]); ?>
<div class="md:flex md:items-center md:justify-between md:gap-2">
<div class="min-w-0">
<div class="inline-block rounded-full bg-red-500/20 px-3 py-2 max-w-full text-sm font-bold leading-5 text-red-500 truncate lg:text-base dark:bg-red-500/20">
<span class="hidden md:inline">
<?php echo e($exception->class()); ?>
</span>
<span class="md:hidden">
<?php echo e(implode(' ', array_slice(explode('\\', $exception->class()), -1))); ?>
</span>
</div>
<div class="mt-4 text-lg font-semibold text-gray-900 break-words dark:text-white lg:text-2xl">
<?php echo e($exception->message()); ?>
</div>
</div>
<div class="hidden text-right shrink-0 md:block md:min-w-64 md:max-w-80">
<div>
<span class="inline-block rounded-full bg-gray-200 px-3 py-2 text-sm leading-5 text-gray-900 max-w-full truncate dark:bg-gray-800 dark:text-white">
<?php echo e($exception->request()->method()); ?> <?php echo e($exception->request()->httpHost()); ?>
</span>
</div>
<div class="px-4">
<span class="text-sm text-gray-500 dark:text-gray-400">PHP <?php echo e(PHP_VERSION); ?> — Laravel <?php echo e(app()->version()); ?></span>
</div>
</div>
</div>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $attributes = $__attributesOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $component = $__componentOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__componentOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/header.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,33 @@
<?php $__currentLoopData = $exception->frames(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $frame): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<div
class="sm:col-span-2"
x-show="index === <?php echo e($loop->index); ?>"
>
<div class="mb-3">
<div class="text-md text-gray-500 dark:text-gray-400">
<div class="mb-2">
<?php if(config('app.editor')): ?>
<a href="<?php echo e($frame->editorHref()); ?>" class="text-blue-500 hover:underline">
<span class="wrap text-gray-900 dark:text-gray-300"><?php echo e($frame->file()); ?></span>
</a>
<?php else: ?>
<span class="wrap text-gray-900 dark:text-gray-300"><?php echo e($frame->file()); ?></span>
<?php endif; ?>
<span class="font-mono text-xs">:<?php echo e($frame->line()); ?></span>
</div>
</div>
</div>
<div class="pt-4 text-sm text-gray-500 dark:text-gray-400">
<pre class="h-[32.5rem] rounded-md dark:bg-gray-800 border dark:border-gray-700"><template x-if="true"><code
style="display: none;"
id="frame-<?php echo e($loop->index); ?>"
class="language-php highlightable-code <?php if($loop->index === $exception->defaultFrame()): ?> default-highlightable-code <?php endif; ?> scrollbar-hidden overflow-y-hidden"
data-line-number="<?php echo e($frame->line()); ?>"
data-ln-start-from="<?php echo e(max($frame->line() - 5, 1)); ?>"
><?php echo e($frame->snippet()); ?></code></template></pre>
</div>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/editor.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,60 @@
<?php use \Illuminate\Foundation\Exceptions\Renderer\Renderer; ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<title><?php echo e(config('app.name', 'Laravel')); ?></title>
<link rel="icon" type="image/svg+xml"
href="data:image/svg+xml,%3Csvg viewBox='0 -.11376601 49.74245785 51.31690859' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m49.626 11.564a.809.809 0 0 1 .028.209v10.972a.8.8 0 0 1 -.402.694l-9.209 5.302v10.509c0 .286-.152.55-.4.694l-19.223 11.066c-.044.025-.092.041-.14.058-.018.006-.035.017-.054.022a.805.805 0 0 1 -.41 0c-.022-.006-.042-.018-.063-.026-.044-.016-.09-.03-.132-.054l-19.219-11.066a.801.801 0 0 1 -.402-.694v-32.916c0-.072.01-.142.028-.21.006-.023.02-.044.028-.067.015-.042.029-.085.051-.124.015-.026.037-.047.055-.071.023-.032.044-.065.071-.093.023-.023.053-.04.079-.06.029-.024.055-.05.088-.069h.001l9.61-5.533a.802.802 0 0 1 .8 0l9.61 5.533h.002c.032.02.059.045.088.068.026.02.055.038.078.06.028.029.048.062.072.094.017.024.04.045.054.071.023.04.036.082.052.124.008.023.022.044.028.068a.809.809 0 0 1 .028.209v20.559l8.008-4.611v-10.51c0-.07.01-.141.028-.208.007-.024.02-.045.028-.068.016-.042.03-.085.052-.124.015-.026.037-.047.054-.071.024-.032.044-.065.072-.093.023-.023.052-.04.078-.06.03-.024.056-.05.088-.069h.001l9.611-5.533a.801.801 0 0 1 .8 0l9.61 5.533c.034.02.06.045.09.068.025.02.054.038.077.06.028.029.048.062.072.094.018.024.04.045.054.071.023.039.036.082.052.124.009.023.022.044.028.068zm-1.574 10.718v-9.124l-3.363 1.936-4.646 2.675v9.124l8.01-4.611zm-9.61 16.505v-9.13l-4.57 2.61-13.05 7.448v9.216zm-36.84-31.068v31.068l17.618 10.143v-9.214l-9.204-5.209-.003-.002-.004-.002c-.031-.018-.057-.044-.086-.066-.025-.02-.054-.036-.076-.058l-.002-.003c-.026-.025-.044-.056-.066-.084-.02-.027-.044-.05-.06-.078l-.001-.003c-.018-.03-.029-.066-.042-.1-.013-.03-.03-.058-.038-.09v-.001c-.01-.038-.012-.078-.016-.117-.004-.03-.012-.06-.012-.09v-21.483l-4.645-2.676-3.363-1.934zm8.81-5.994-8.007 4.609 8.005 4.609 8.006-4.61-8.006-4.608zm4.164 28.764 4.645-2.674v-20.096l-3.363 1.936-4.646 2.675v20.096zm24.667-23.325-8.006 4.609 8.006 4.609 8.005-4.61zm-.801 10.605-4.646-2.675-3.363-1.936v9.124l4.645 2.674 3.364 1.937zm-18.422 20.561 11.743-6.704 5.87-3.35-8-4.606-9.211 5.303-8.395 4.833z' fill='%23ff2d20'/%3E%3C/svg%3E" />
<link
href="https://fonts.bunny.net/css?family=figtree:300,400,500,600"
rel="stylesheet"
/>
<?php echo Renderer::css(); ?>
<style>
<?php $__currentLoopData = $exception->frames(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $frame): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
#frame-<?php echo e($loop->index); ?> .hljs-ln-line[data-line-number='<?php echo e($frame->line()); ?>'] {
background-color: rgba(242, 95, 95, 0.4);
}
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</style>
</head>
<body class="bg-gray-200/80 font-sans antialiased dark:bg-gray-950/95">
<?php echo e($slot); ?>
<?php echo Renderer::js(); ?>
<script>
!function(r,o){"use strict";var e,i="hljs-ln",l="hljs-ln-line",h="hljs-ln-code",s="hljs-ln-numbers",c="hljs-ln-n",m="data-line-number",a=/\r\n|\r|\n/g;function u(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var o=parseInt(t.dataset.lineNumber),a=parseInt(r.dataset.lineNumber);if(o==a)return n;var i,l=t.textContent,s=r.textContent;for(a<o&&(i=o,o=a,a=i,i=l,l=s,s=i);0!==n.indexOf(l);)l=l.slice(1);for(;-1===n.lastIndexOf(s);)s=s.slice(0,-1);for(var c=l,u=function(e){for(var n=e;"TABLE"!==n.nodeName;)n=n.parentNode;return n}(t),d=o+1;d<a;++d){var f=p('.{0}[{1}="{2}"]',[h,m,d]);c+="\n"+u.querySelector(f).textContent}return c+="\n"+s}function n(e){try{var n=o.querySelectorAll("code.hljs,code.nohighlight");for(var t in n)n.hasOwnProperty(t)&&(n[t].classList.contains("nohljsln")||d(n[t],e))}catch(e){r.console.error("LineNumbers error: ",e)}}function d(e,n){"object"==typeof e&&r.setTimeout(function(){e.innerHTML=f(e,n)},0)}function f(e,n){var t,r,o=(t=e,{singleLine:function(e){return!!e.singleLine&&e.singleLine}(r=(r=n)||{}),startFrom:function(e,n){var t=1;isFinite(n.startFrom)&&(t=n.startFrom);var r=function(e,n){return e.hasAttribute(n)?e.getAttribute(n):null}(e,"data-ln-start-from");return null!==r&&(t=function(e,n){if(!e)return n;var t=Number(e);return isFinite(t)?t:n}(r,1)),t}(t,r)});return function e(n){var t=n.childNodes;for(var r in t){var o;t.hasOwnProperty(r)&&(o=t[r],0<(o.textContent.trim().match(a)||[]).length&&(0<o.childNodes.length?e(o):v(o.parentNode)))}}(e),function(e,n){var t=g(e);""===t[t.length-1].trim()&&t.pop();if(1<t.length||n.singleLine){for(var r="",o=0,a=t.length;o<a;o++)r+=p('<tr><td class="{0} {1}" {3}="{5}"><div class="{2}" {3}="{5}"></div></td><td class="{0} {4}" {3}="{5}">{6}</td></tr>',[l,s,c,m,h,o+n.startFrom,0<t[o].length?t[o]:" "]);return p('<table class="{0}">{1}</table>',[i,r])}return e}(e.innerHTML,o)}function v(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r<t.length;r++){o+=p('<span class="{0}">{1}</span>\n',[n,0<t[r].length?t[r]:" "])}e.innerHTML=o.trim()}}function g(e){return 0===e.length?[]:e.split(a)}function p(e,t){return e.replace(/\{(\d+)\}/g,function(e,n){return void 0!==t[n]?t[n]:e})}r.hljs?(r.hljs.initLineNumbersOnLoad=function(e){"interactive"===o.readyState||"complete"===o.readyState?n(e):r.addEventListener("DOMContentLoaded",function(){n(e)})},r.hljs.lineNumbersBlock=d,r.hljs.lineNumbersValue=function(e,n){if("string"!=typeof e)return;var t=document.createElement("code");return t.innerHTML=e,f(t,n)},(e=o.createElement("style")).type="text/css",e.innerHTML=p(".{0}{border-collapse:collapse}.{0} td{padding:0}.{1}:before{content:attr({2})}",[i,c,m]),o.getElementsByTagName("head")[0].appendChild(e)):r.console.error("highlight.js not detected!"),document.addEventListener("copy",function(e){var n,t=window.getSelection();!function(e){for(var n=e;n;){if(n.className&&-1!==n.className.indexOf("hljs-ln-code"))return 1;n=n.parentNode}}(t.anchorNode)||(n=-1!==window.navigator.userAgent.indexOf("Edge")?u(t):t.toString(),e.clipboardData.setData("text/plain",n),e.preventDefault())})}(window,document);
hljs.initLineNumbersOnLoad()
window.addEventListener('load', function() {
document.querySelectorAll('.renderer').forEach(function(element, index) {
if (index > 0) {
element.remove();
}
});
document.querySelector('.default-highlightable-code').style.display = 'block';
document.querySelectorAll('.highlightable-code').forEach(function(element) {
element.style.display = 'block';
})
});
</script>
</body>
</html>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/layout.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,117 @@
<?php if($paginator->hasPages()): ?>
<nav role="navigation" aria-label="<?php echo e(__('Pagination Navigation')); ?>" class="flex items-center justify-between">
<div class="flex justify-between flex-1 sm:hidden">
<?php if($paginator->onFirstPage()): ?>
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
<?php echo __('pagination.previous'); ?>
</span>
<?php else: ?>
<a href="<?php echo e($paginator->previousPageUrl()); ?>" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
<?php echo __('pagination.previous'); ?>
</a>
<?php endif; ?>
<?php if($paginator->hasMorePages()): ?>
<a href="<?php echo e($paginator->nextPageUrl()); ?>" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
<?php echo __('pagination.next'); ?>
</a>
<?php else: ?>
<span class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
<?php echo __('pagination.next'); ?>
</span>
<?php endif; ?>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 leading-5 dark:text-gray-400">
<?php echo __('Showing'); ?>
<?php if($paginator->firstItem()): ?>
<span class="font-medium"><?php echo e($paginator->firstItem()); ?></span>
<?php echo __('to'); ?>
<span class="font-medium"><?php echo e($paginator->lastItem()); ?></span>
<?php else: ?>
<?php echo e($paginator->count()); ?>
<?php endif; ?>
<?php echo __('of'); ?>
<span class="font-medium"><?php echo e($paginator->total()); ?></span>
<?php echo __('results'); ?>
</p>
</div>
<div>
<span class="relative z-0 inline-flex rtl:flex-row-reverse shadow-sm rounded-md">
<?php if($paginator->onFirstPage()): ?>
<span aria-disabled="true" aria-label="<?php echo e(__('pagination.previous')); ?>">
<span class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-l-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
</span>
<?php else: ?>
<a href="<?php echo e($paginator->previousPageUrl()); ?>" rel="prev" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="<?php echo e(__('pagination.previous')); ?>">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</a>
<?php endif; ?>
<?php $__currentLoopData = $elements; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $element): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if(is_string($element)): ?>
<span aria-disabled="true">
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600"><?php echo e($element); ?></span>
</span>
<?php endif; ?>
<?php if(is_array($element)): ?>
<?php $__currentLoopData = $element; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $page => $url): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if($page == $paginator->currentPage()): ?>
<span aria-current="page">
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600"><?php echo e($page); ?></span>
</span>
<?php else: ?>
<a href="<?php echo e($url); ?>" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-300 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="<?php echo e(__('Go to page :page', ['page' => $page])); ?>">
<?php echo e($page); ?>
</a>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php if($paginator->hasMorePages()): ?>
<a href="<?php echo e($paginator->nextPageUrl()); ?>" rel="next" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="<?php echo e(__('pagination.next')); ?>">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</a>
<?php else: ?>
<span aria-disabled="true" aria-label="<?php echo e(__('pagination.next')); ?>">
<span class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-r-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</span>
</span>
<?php endif; ?>
</span>
</div>
</div>
</nav>
<?php endif; ?>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Pagination/resources/views/tailwind.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,186 @@
<?php use \Illuminate\Support\Str; ?>
<?php if (isset($component)) { $__componentOriginal74daf2d0a9c625ad90327a6043d15980 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal74daf2d0a9c625ad90327a6043d15980 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.card','data' => ['class' => 'mt-6 overflow-x-auto']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::card'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'mt-6 overflow-x-auto']); ?>
<div>
<span class="text-xl font-bold lg:text-2xl">Request</span>
</div>
<div class="mt-2">
<span><?php echo e($exception->request()->method()); ?></span>
<span class="text-gray-500"><?php echo e(Str::start($exception->request()->path(), '/')); ?></span>
</div>
<div class="mt-4">
<span class="font-semibold text-gray-900 dark:text-white">Headers</span>
</div>
<dl class="mt-1 grid grid-cols-1 rounded border dark:border-gray-800">
<?php $__empty_1 = true; $__currentLoopData = $exception->requestHeaders(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $key => $value): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<div class="flex items-center gap-2 <?php echo e($loop->first ? '' : 'border-t'); ?> dark:border-gray-800">
<span
data-tippy-content="<?php echo e($key); ?>"
class="lg:text-md w-[8rem] flex-none cursor-pointer truncate border-r px-5 py-3 text-sm dark:border-gray-800 lg:w-[12rem]"
>
<?php echo e($key); ?>
</span>
<span
class="min-w-0 flex-grow"
style="
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem));
"
>
<pre class="scrollbar-hidden overflow-y-hidden text-xs lg:text-sm"><code class="px-5 py-3 overflow-y-hidden scrollbar-hidden max-h-32 overflow-x-scroll scrollbar-hidden-x"><?php echo e($value); ?></code></pre>
</span>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<span
class="min-w-0 flex-grow"
style="-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem))"
>
<pre class="scrollbar-hidden mx-5 my-3 overflow-y-hidden text-xs lg:text-sm"><code class="overflow-y-hidden scrollbar-hidden overflow-x-scroll scrollbar-hidden-x">No headers data</code></pre>
</span>
<?php endif; ?>
</dl>
<div class="mt-4">
<span class="font-semibold text-gray-900 dark:text-white">Body</span>
</div>
<div class="mt-1 rounded border dark:border-gray-800">
<div class="flex items-center">
<span
class="min-w-0 flex-grow"
style="-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem))"
>
<pre class="scrollbar-hidden mx-5 my-3 overflow-y-hidden text-xs lg:text-sm"><code class="overflow-y-hidden scrollbar-hidden overflow-x-scroll scrollbar-hidden-x"><?php echo e($exception->requestBody() ?: 'No body data'); ?></code></pre>
</span>
</div>
</div>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $attributes = $__attributesOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $component = $__componentOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__componentOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginal74daf2d0a9c625ad90327a6043d15980 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal74daf2d0a9c625ad90327a6043d15980 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.card','data' => ['class' => 'mt-6 overflow-x-auto']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::card'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'mt-6 overflow-x-auto']); ?>
<div>
<span class="text-xl font-bold lg:text-2xl">Application</span>
</div>
<div class="mt-4">
<span class="font-semibold text-gray-900 dark:text-white"> Routing </span>
</div>
<dl class="mt-1 grid grid-cols-1 rounded border dark:border-gray-800">
<?php $__empty_1 = true; $__currentLoopData = $exception->applicationRouteContext(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $name => $value): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<div class="flex items-center gap-2 <?php echo e($loop->first ? '' : 'border-t'); ?> dark:border-gray-800">
<span
data-tippy-content="<?php echo e($name); ?>"
class="lg:text-md w-[8rem] flex-none cursor-pointer truncate border-r px-5 py-3 text-sm dark:border-gray-800 lg:w-[12rem]"
><?php echo e($name); ?></span
>
<span
class="min-w-0 flex-grow"
style="
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem));
"
>
<pre class="scrollbar-hidden overflow-y-hidden text-xs lg:text-sm"><code class="px-5 py-3 overflow-y-hidden scrollbar-hidden max-h-32 overflow-x-scroll scrollbar-hidden-x"><?php echo e($value); ?></code></pre>
</span>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<span
class="min-w-0 flex-grow"
style="-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem))"
>
<pre class="scrollbar-hidden mx-5 my-3 overflow-y-hidden text-xs lg:text-sm"><code class="overflow-y-hidden scrollbar-hidden overflow-x-scroll scrollbar-hidden-x">No routing data</code></pre>
</span>
<?php endif; ?>
</dl>
<?php if($routeParametersContext = $exception->applicationRouteParametersContext()): ?>
<div class="mt-4">
<span class="text-gray-900 dark:text-white text-sm"> Routing Parameters </span>
</div>
<div class="mt-1 rounded border dark:border-gray-800">
<div class="flex items-center">
<span
class="min-w-0 flex-grow"
style="-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem))"
>
<pre class="scrollbar-hidden mx-5 my-3 overflow-y-hidden text-xs lg:text-sm"><code class="overflow-y-hidden scrollbar-hidden overflow-x-scroll scrollbar-hidden-x"><?php echo e($routeParametersContext); ?></code></pre>
</span>
</div>
</div>
<?php endif; ?>
<div class="mt-4">
<span class="font-semibold text-gray-900 dark:text-white"> Database Queries </span>
<span class="text-xs text-gray-500 dark:text-gray-400">
<?php if(count($exception->applicationQueries()) === 100): ?>
only the first 100 queries are displayed
<?php endif; ?>
</span>
</div>
<dl class="mt-1 grid grid-cols-1 rounded border dark:border-gray-800">
<?php $__empty_1 = true; $__currentLoopData = $exception->applicationQueries(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as ['connectionName' => $connectionName, 'sql' => $sql, 'time' => $time]): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<div class="flex items-center gap-2 <?php echo e($loop->first ? '' : 'border-t'); ?> dark:border-gray-800">
<div class="lg:text-md w-[8rem] flex-none truncate border-r px-5 py-3 text-sm dark:border-gray-800 lg:w-[12rem]">
<span><?php echo e($connectionName); ?></span>
<span class="hidden text-xs text-gray-500 lg:inline-block">(<?php echo e($time); ?> ms)</span>
</div>
<span
class="min-w-0 flex-grow"
style="
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem));
"
>
<pre class="scrollbar-hidden overflow-y-hidden text-xs lg:text-sm"><code class="px-5 py-3 overflow-y-hidden scrollbar-hidden max-h-32 overflow-x-scroll scrollbar-hidden-x"><?php echo e($sql); ?></code></pre>
</span>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<span
class="min-w-0 flex-grow"
style="-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 1rem, #000 calc(100% - 3rem), transparent calc(100% - 1rem))"
>
<pre class="scrollbar-hidden mx-5 my-3 overflow-y-hidden text-xs lg:text-sm"><code class="overflow-y-hidden scrollbar-hidden overflow-x-scroll scrollbar-hidden-x">No query data</code></pre>
</span>
<?php endif; ?>
</dl>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $attributes = $__attributesOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $component = $__componentOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__componentOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/context.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
<?php echo e($attributes); ?>
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
</svg>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/icons/computer-desktop.blade.php ENDPATH**/ ?>

After

Width:  |  Height:  |  Size: 700 B

View File

@ -0,0 +1,442 @@
<?php $__env->startSection('content'); ?>
<div class="space-y-6">
<?php if(session('success')): ?>
<div class="bg-green-50 border border-green-200 text-green-700 px-5 py-4 rounded-2xl">
<?php echo e(session('success')); ?>
</div>
<?php endif; ?>
<?php if(session('error')): ?>
<div class="bg-red-50 border border-red-200 text-red-700 px-5 py-4 rounded-2xl">
<?php echo e(session('error')); ?>
</div>
<?php endif; ?>
<!-- HEADER -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-slate-800">Kelola Pengguna</h1>
<p class="text-slate-500 mt-1">Kelola akun pengguna sistem SENTARA.</p>
</div>
<!-- BUTTON TAMBAH -->
<button
onclick="openTambahModal()"
class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-3 rounded-2xl text-sm font-semibold shadow transition">
+ Tambah Pengguna
</button>
</div>
<!-- CARD -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-100 p-5">
<!-- TABLE -->
<div class="overflow-x-auto mt-6">
<table class="w-full text-sm min-w-[900px]">
<thead>
<tr class="text-slate-500 border-b">
<th class="py-4 text-left">No</th>
<th class="py-4 text-left">Nama</th>
<th class="py-4 text-left">Email</th>
<th class="py-4 text-left">Role</th>
<th class="py-4 text-left">Status</th>
<th class="py-4 text-left">Terakhir Aktif</th>
<th class="py-4 text-center">Aksi</th>
</tr>
</thead>
<tbody class="text-slate-700">
<?php $__empty_1 = true; $__currentLoopData = $users; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $user): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<tr class="border-b hover:bg-slate-50 transition">
<td class="py-4"><?php echo e($loop->iteration); ?></td>
<td class="py-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center text-xs font-bold text-slate-600">
<?php echo e(strtoupper(substr($user->name, 0, 2))); ?>
</div>
<span class="font-medium"><?php echo e($user->name); ?></span>
</div>
</td>
<td class="py-4"><?php echo e($user->email); ?></td>
<td class="py-4">
<?php if($user->role == 'admin'): ?>
<span class="bg-purple-100 text-purple-700 px-3 py-1 rounded-full text-xs font-semibold">Admin</span>
<?php else: ?>
<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-xs font-semibold">Staff</span>
<?php endif; ?>
</td>
<td class="py-4">
<span class="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-semibold">Aktif</span>
</td>
<td class="py-4"><?php echo e($user->updated_at->format('d M Y, H:i')); ?></td>
<td class="py-4">
<div class="flex justify-center gap-2">
<button
onclick="openEditModal('<?php echo e($user->id); ?>', '<?php echo e(addslashes($user->name)); ?>', '<?php echo e($user->email); ?>', '<?php echo e($user->role); ?>')"
class="w-11 h-11 rounded-2xl border border-blue-200 text-blue-600 hover:bg-blue-50 flex items-center justify-center transition"
title="Edit Pengguna">✏️</button>
<button
onclick="openPasswordModal('<?php echo e($user->id); ?>')"
class="w-11 h-11 rounded-2xl border border-amber-200 text-amber-500 hover:bg-amber-50 flex items-center justify-center transition"
title="Reset Password">🔒</button>
<button
onclick="openDeleteModal('<?php echo e($user->id); ?>', '<?php echo e($user->role); ?>')"
class="w-11 h-11 rounded-2xl border border-red-200 text-red-500 hover:bg-red-50 flex items-center justify-center transition"
title="Hapus Pengguna">🗑️</button>
</div>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<tr>
<td colspan="7" class="py-10 text-center text-slate-400">
Data pengguna belum tersedia.
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- PAGINATION -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mt-6">
<p class="text-sm text-slate-500">
Menampilkan <?php echo e($users->firstItem() ?? 0); ?> - <?php echo e($users->lastItem() ?? 0); ?>
dari <?php echo e($users->total()); ?> pengguna
</p>
<?php echo e($users->links()); ?>
</div>
</div>
</div>
<!-- ================= MODAL TAMBAH PENGGUNA ================= -->
<div id="tambahModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full" style="max-width: 480px;">
<div class="flex items-start justify-between mb-5">
<div>
<h2 class="text-2xl font-bold text-slate-800">Tambah Pengguna</h2>
<p class="text-slate-500 text-sm mt-1">Buat akun pengguna baru untuk sistem SENTARA.</p>
</div>
<button onclick="closeTambahModal()"
class="text-slate-400 hover:text-slate-700 text-2xl leading-none ml-4 flex-shrink-0">×</button>
</div>
<form method="POST" action="/kelola-pengguna" id="tambahForm">
<?php echo csrf_field(); ?>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Nama Pengguna</label>
<input type="text" name="name" placeholder="Masukkan nama lengkap"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Email</label>
<input type="email" name="email" placeholder="contoh@email.com"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Role Pengguna</label>
<select name="role"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
<option value="staff">Staff</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Password</label>
<input type="password" name="password" placeholder="Minimal 8 karakter"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-slate-700 mb-2">Konfirmasi Password</label>
<input type="password" name="password_confirmation" placeholder="Ulangi password"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" required>
</div>
<div class="flex gap-3">
<button type="button" onclick="closeTambahModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold transition">
Tambah
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL EDIT ================= -->
<div id="editModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full" style="max-width: 480px;">
<div class="flex items-start justify-between mb-5">
<div>
<h2 class="text-2xl font-bold text-slate-800">Edit Pengguna</h2>
<p class="text-slate-500 text-sm mt-1">Perbarui informasi akun pengguna SENTARA.</p>
</div>
<button onclick="closeEditModal()"
class="text-slate-400 hover:text-slate-700 text-2xl leading-none ml-4 flex-shrink-0">×</button>
</div>
<form method="POST" id="editForm">
<?php echo csrf_field(); ?>
<?php if($errors->any()): ?>
<div class="mb-4 rounded-2xl bg-red-50 border border-red-200 text-red-600 px-4 py-3 text-sm">
<?php echo e($errors->first()); ?>
</div>
<?php endif; ?>
<?php echo method_field('PUT'); ?>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Nama Pengguna</label>
<input type="text" name="name" id="editName"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Email</label>
<input type="email" name="email" id="editEmail"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-slate-700 mb-2">Role Pengguna</label>
<select name="role" id="editRole"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none">
<option value="admin">Admin</option>
<option value="staff">Staff</option>
</select>
</div>
<div class="flex gap-3">
<button type="button" onclick="closeEditModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold transition">
Simpan
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL RESET PASSWORD ================= -->
<div id="passwordModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full" style="max-width: 480px;">
<div class="flex items-start justify-between mb-5">
<div>
<h2 class="text-2xl font-bold text-slate-800">Reset Password</h2>
<p class="text-slate-500 text-sm mt-1">Buat password baru pengguna.</p>
</div>
<button onclick="closePasswordModal()"
class="text-slate-400 hover:text-slate-700 text-2xl leading-none ml-4 flex-shrink-0">×</button>
</div>
<div class="mb-5 bg-amber-50 border border-amber-200 rounded-2xl p-4">
<p class="text-sm text-amber-700 leading-relaxed">
Password minimal 8 karakter dan harus kombinasi huruf besar, huruf kecil, dan angka.
</p>
</div>
<form method="POST" id="passwordForm">
<?php echo csrf_field(); ?>
<?php echo method_field('PUT'); ?>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Password Baru</label>
<input type="password" name="password"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-amber-400 focus:outline-none">
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-slate-700 mb-2">Konfirmasi Password</label>
<input type="password" name="password_confirmation"
class="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm focus:ring-2 focus:ring-amber-400 focus:outline-none">
</div>
<div class="flex gap-3">
<button type="button" onclick="closePasswordModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold transition">
Reset
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL DELETE ================= -->
<div id="deleteModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full text-center" style="max-width: 400px;">
<div class="w-16 h-16 mx-auto rounded-full bg-red-100 flex items-center justify-center text-3xl mb-4">🗑️</div>
<h2 class="text-2xl font-bold text-slate-800">Hapus Pengguna?</h2>
<p class="text-slate-500 text-sm mt-2 leading-relaxed">
Data pengguna akan dihapus permanen dan tidak dapat dikembalikan.
</p>
<form method="POST" id="deleteForm" class="mt-6">
<?php echo csrf_field(); ?>
<?php echo method_field('DELETE'); ?>
<div class="flex gap-3">
<button type="button" onclick="closeDeleteModal()"
class="flex-1 py-3 rounded-2xl border border-slate-200 text-slate-600 text-sm font-medium hover:bg-slate-100 transition">
Batal
</button>
<button type="submit"
class="flex-1 py-3 rounded-2xl bg-red-500 hover:bg-red-600 text-white text-sm font-semibold transition">
Hapus
</button>
</div>
</form>
</div>
</div>
<!-- ================= MODAL ADMIN TIDAK BISA DIHAPUS ================= -->
<div id="adminAlertModal"
class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center px-4">
<div class="bg-white rounded-3xl p-6 shadow-2xl w-full text-center" style="max-width: 400px;">
<div class="w-16 h-16 mx-auto rounded-full bg-amber-100 flex items-center justify-center text-3xl mb-4">⚠️</div>
<h2 class="text-2xl font-bold text-slate-800">Akses Ditolak</h2>
<p class="text-slate-500 text-sm mt-2 leading-relaxed">
Akun administrator tidak dapat dihapus demi menjaga keamanan sistem.
</p>
<button onclick="closeAdminAlertModal()"
class="w-full mt-6 py-3 rounded-2xl bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold transition">
Mengerti
</button>
</div>
</div>
<!-- ================= SCRIPT ================= -->
<script>
/* ---- TAMBAH ---- */
function openTambahModal() {
document.getElementById('tambahModal').classList.remove('hidden');
document.getElementById('tambahForm').reset();
}
function closeTambahModal() {
document.getElementById('tambahModal').classList.add('hidden');
}
/* ---- EDIT ---- */
function openEditModal(id, name, email, role) {
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editName').value = name;
document.getElementById('editEmail').value = email;
document.getElementById('editRole').value = role;
document.getElementById('editForm').action = '/kelola-pengguna/' + id;
}
function closeEditModal() {
document.getElementById('editModal').classList.add('hidden');
}
/* ---- RESET PASSWORD ---- */
function openPasswordModal(id) {
document.getElementById('passwordModal').classList.remove('hidden');
document.getElementById('passwordForm').action =
'/kelola-pengguna/' + id + '/reset-password';
}
function closePasswordModal() {
document.getElementById('passwordModal').classList.add('hidden');
}
/* ---- DELETE ---- */
function openDeleteModal(id, role) {
if (role === 'admin') {
document.getElementById('adminAlertModal').classList.remove('hidden');
return;
}
document.getElementById('deleteModal').classList.remove('hidden');
document.getElementById('deleteForm').action = '/kelola-pengguna/' + id;
}
function closeDeleteModal() {
document.getElementById('deleteModal').classList.add('hidden');
}
function closeAdminAlertModal() {
document.getElementById('adminAlertModal').classList.add('hidden');
}
</script>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/kelolauser/index.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,222 @@
<?php
$destinasiList ??= collect([]);
$totalDestinasiAnalisis ??= 0;
$totalDestinasiBerkeluhan ??= 0;
$totalNegatif ??= 0;
$totalUlasan ??= 0;
$persenNegatif ??= 0;
$tingkatKepuasan ??= 0;
$labelKepuasan ??= 'Kurang';
$isuDominan ??= '-';
$isuDominanPersen ??= 0;
$isuUtama ??= [];
$kataDominan ??= [];
$saranPerbaikan ??= [];
$prioritas ??= [];
?>
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-800">Rekomendasi Layanan</h2>
<p class="text-sm text-gray-500">
Rekomendasi dibuat dari ulasan negatif agar saran perbaikan fokus pada keluhan pengunjung.
</p>
<p class="text-xs text-gray-400 mt-1">
<?php echo e($totalDestinasiAnalisis); ?> destinasi dianalisis, <?php echo e($totalDestinasiBerkeluhan); ?> destinasi memiliki ulasan negatif.
</p>
</div>
<form method="GET" action="<?php echo e(route('dashboard')); ?>" class="flex gap-3">
<input type="hidden" name="tab" value="rekomendasi">
<input type="hidden" name="periode_id" value="<?php echo e($periodeAktif->id ?? request('periode_id')); ?>">
<select name="destinasi" onchange="this.form.submit()"
class="border rounded-lg pl-3 pr-9 py-2 text-sm min-w-[220px] focus:outline-none focus:ring-2 focus:ring-blue-400">
<option value="">Semua Destinasi</option>
<?php $__currentLoopData = $destinasiList; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $destinasi): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option value="<?php echo e($destinasi); ?>" <?php echo e(request('destinasi') == $destinasi ? 'selected' : ''); ?>>
<?php echo e($destinasi); ?>
</option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</select>
</form>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-5">
<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">Destinasi Dianalisis</p>
<h3 class="text-xl font-bold"><?php echo e($totalDestinasiAnalisis); ?></h3>
<p class="text-xs text-gray-500"><?php echo e($totalDestinasiBerkeluhan); ?> punya keluhan</p>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
<div class="bg-red-100 text-red-500 p-3 rounded-full text-xl">😟</div>
<div>
<p class="text-sm text-gray-500">Total Ulasan Negatif</p>
<h3 class="text-xl font-bold"><?php echo e($totalNegatif); ?></h3>
<p class="text-xs text-red-500"><?php echo e($persenNegatif); ?>% dari total</p>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
<div class="bg-green-100 text-green-600 p-3 rounded-full text-xl">😊</div>
<div>
<p class="text-sm text-gray-500">Tingkat Kepuasan</p>
<h3 class="text-xl font-bold"><?php echo e($tingkatKepuasan); ?>%</h3>
<span class="text-xs px-2 py-1 rounded
<?php echo e($labelKepuasan === 'Baik' ? 'bg-green-100 text-green-700' :
($labelKepuasan === 'Sedang' ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700')); ?>">
<?php echo e($labelKepuasan); ?>
</span>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
<div class="bg-purple-100 text-purple-600 p-3 rounded-full text-xl"></div>
<div>
<p class="text-sm text-gray-500">Isu Dominan</p>
<h3 class="text-xl font-bold"><?php echo e($isuDominan); ?></h3>
<p class="text-xs text-gray-500"><?php echo e($isuDominanPersen); ?>% dari total isu</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="bg-white rounded-xl shadow p-5">
<h3 class="font-semibold mb-4 text-gray-700">Isu Utama (Top 5)</h3>
<?php $__empty_1 = true; $__currentLoopData = $isuUtama; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $i => $isu): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<?php
$warnaClass = match($isu['color']) {
'red' => 'bg-red-500',
'orange' => 'bg-orange-500',
'yellow' => 'bg-yellow-400',
'green' => 'bg-green-500',
default => 'bg-blue-500',
};
?>
<div class="mb-4">
<div class="flex justify-between text-sm mb-1">
<span><?php echo e($i + 1); ?>. <?php echo e($isu['nama']); ?></span>
<span class="font-semibold"><?php echo e($isu['persen']); ?>%</span>
</div>
<div class="w-full bg-gray-200 h-2 rounded">
<div class="h-2 rounded <?php echo e($warnaClass); ?>" style="width: <?php echo e($isu['persen']); ?>%"></div>
</div>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<p class="text-sm text-gray-400 text-center py-4">Belum ada data isu.</p>
<?php endif; ?>
</div>
<div class="bg-white rounded-xl shadow p-5">
<h3 class="font-semibold mb-4 text-gray-700">Kata Kunci Dominan</h3>
<?php $maxFreq = !empty($kataDominan) ? max($kataDominan) : 1; ?>
<?php $__empty_1 = true; $__currentLoopData = array_slice($kataDominan, 0, 5, true); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $kata => $jumlah): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<div class="mb-3">
<div class="flex justify-between text-sm">
<span><?php echo e($kata); ?></span>
<span class="font-semibold text-blue-600"><?php echo e($jumlah); ?>x</span>
</div>
<div class="w-full bg-gray-200 h-2 rounded mt-1">
<div class="bg-blue-500 h-2 rounded" style="width: <?php echo e(round(($jumlah / $maxFreq) * 100)); ?>%"></div>
</div>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<p class="text-sm text-gray-400 text-center py-4">Belum ada kata kunci.</p>
<?php endif; ?>
</div>
<div class="bg-white rounded-xl shadow p-5">
<h3 class="font-semibold mb-4 text-gray-700">Saran Perbaikan</h3>
<div class="space-y-4 text-sm">
<?php $__empty_1 = true; $__currentLoopData = $saranPerbaikan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $saran): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<div class="flex gap-3">
<div class="text-xl"><?php echo e($saran['icon']); ?></div>
<div>
<p class="font-semibold"><?php echo e($saran['nama']); ?></p>
<p class="text-gray-600"><?php echo e($saran['tip']); ?></p>
</div>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<p class="text-sm text-gray-400 text-center py-4">Belum ada saran.</p>
<?php endif; ?>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<?php $__empty_1 = true; $__currentLoopData = $prioritas; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $p): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<?php
$border = match($p['color']) {
'red' => 'border-red-300',
'orange' => 'border-orange-300',
'yellow' => 'border-yellow-300',
'green' => 'border-green-300',
default => 'border-blue-300',
};
$title = match($p['color']) {
'red' => 'text-red-600',
'orange' => 'text-orange-500',
'yellow' => 'text-yellow-600',
'green' => 'text-green-600',
default => 'text-blue-600',
};
$dampak = match($p['color']) {
'red' => 'bg-red-100 text-red-600',
'orange' => 'bg-orange-100 text-orange-600',
'yellow' => 'bg-yellow-100 text-yellow-600',
'green' => 'bg-green-100 text-green-700',
default => 'bg-blue-100 text-blue-600',
};
?>
<div class="border <?php echo e($border); ?> rounded-xl p-5 bg-white">
<h4 class="font-semibold <?php echo e($title); ?> mb-2">Prioritas <?php echo e($p['rank']); ?></h4>
<h3 class="text-lg font-bold mb-3"><?php echo e($p['nama']); ?></h3>
<ul class="text-sm space-y-2 mb-4">
<?php $__currentLoopData = $p['actions']; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $action): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<li class="flex items-center gap-2">
<span class="text-blue-500"></span> <?php echo e($action); ?>
</li>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</ul>
<div class="text-xs <?php echo e($dampak); ?> p-2 rounded">
Dampak: <?php echo e($p['dampak']); ?>
</div>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<div class="col-span-3 text-center text-gray-400 text-sm py-6">
Belum ada data rekomendasi. Pastikan analisis sentimen sudah dijalankan.
</div>
<?php endif; ?>
</div>
</div><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/rekomendasi/index.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4" style="margin-bottom: -8px;">
<path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
</svg>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/icons/chevron-down.blade.php ENDPATH**/ ?>

After

Width:  |  Height:  |  Size: 524 B

View File

@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
<?php echo e($attributes); ?>
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/icons/sun.blade.php ENDPATH**/ ?>

After

Width:  |  Height:  |  Size: 625 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4" style="margin-bottom: -8px;">
<path fill-rule="evenodd" d="M9.47 6.47a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L10 8.06l-3.72 3.72a.75.75 0 0 1-1.06-1.06l4.25-4.25Z" clip-rule="evenodd" />
</svg>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/icons/chevron-up.blade.php ENDPATH**/ ?>

After

Width:  |  Height:  |  Size: 502 B

View File

@ -0,0 +1,194 @@
<script>
(function () {
const darkStyles = document.querySelector('style[data-theme="dark"]')?.textContent
const lightStyles = document.querySelector('style[data-theme="light"]')?.textContent
const removeStyles = () => {
document.querySelector('style[data-theme="dark"]')?.remove()
document.querySelector('style[data-theme="light"]')?.remove()
}
removeStyles()
setDarkClass = () => {
removeStyles()
const isDark = localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
isDark ? document.documentElement.classList.add('dark') : document.documentElement.classList.remove('dark')
if (isDark) {
document.head.insertAdjacentHTML('beforeend', `<style data-theme="dark">${darkStyles}</style>`)
} else {
document.head.insertAdjacentHTML('beforeend', `<style data-theme="light">${lightStyles}</style>`)
}
}
setDarkClass()
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setDarkClass)
})();
</script>
<div
class="relative"
x-data="{
menu: false,
theme: localStorage.theme,
darkMode() {
this.theme = 'dark'
localStorage.theme = 'dark'
setDarkClass()
},
lightMode() {
this.theme = 'light'
localStorage.theme = 'light'
setDarkClass()
},
systemMode() {
this.theme = undefined
localStorage.removeItem('theme')
setDarkClass()
},
}"
@click.outside="menu = false"
>
<button
x-cloak
class="block rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
:class="theme ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400 dark:text-gray-600 hover:text-gray-500 focus:text-gray-500 dark:hover:text-gray-500 dark:focus:text-gray-500'"
@click="menu = ! menu"
>
<?php if (isset($component)) { $__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.sun','data' => ['class' => 'block h-5 w-5 dark:hidden']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.sun'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'block h-5 w-5 dark:hidden']); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7)): ?>
<?php $attributes = $__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7; ?>
<?php unset($__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7); ?>
<?php endif; ?>
<?php if (isset($__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7)): ?>
<?php $component = $__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7; ?>
<?php unset($__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.moon','data' => ['class' => 'hidden h-5 w-5 dark:block']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.moon'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'hidden h-5 w-5 dark:block']); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745)): ?>
<?php $attributes = $__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745; ?>
<?php unset($__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745)): ?>
<?php $component = $__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745; ?>
<?php unset($__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745); ?>
<?php endif; ?>
</button>
<div
x-show="menu"
class="absolute right-0 z-10 flex origin-top-right flex-col rounded-md bg-white shadow-xl ring-1 ring-gray-900/5 dark:bg-gray-800"
style="display: none"
@click="menu = false"
>
<button
class="flex items-center gap-3 px-4 py-2 hover:rounded-t-md hover:bg-gray-100 dark:hover:bg-gray-700"
:class="theme === 'light' ? 'text-gray-900 dark:text-gray-100' : 'text-gray-500 dark:text-gray-400'"
@click="lightMode()"
>
<?php if (isset($component)) { $__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.sun','data' => ['class' => 'h-5 w-5']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.sun'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'h-5 w-5']); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7)): ?>
<?php $attributes = $__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7; ?>
<?php unset($__attributesOriginalbfde029a2e31d1ec96b5017ff81a67a7); ?>
<?php endif; ?>
<?php if (isset($__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7)): ?>
<?php $component = $__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7; ?>
<?php unset($__componentOriginalbfde029a2e31d1ec96b5017ff81a67a7); ?>
<?php endif; ?>
Light
</button>
<button
class="flex items-center gap-3 px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
:class="theme === 'dark' ? 'text-gray-900 dark:text-gray-100' : 'text-gray-500 dark:text-gray-400'"
@click="darkMode()"
>
<?php if (isset($component)) { $__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.moon','data' => ['class' => 'h-5 w-5']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.moon'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'h-5 w-5']); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745)): ?>
<?php $attributes = $__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745; ?>
<?php unset($__attributesOriginal6dda8ad3ea7f20f6c0a87e7037386745); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745)): ?>
<?php $component = $__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745; ?>
<?php unset($__componentOriginal6dda8ad3ea7f20f6c0a87e7037386745); ?>
<?php endif; ?>
Dark
</button>
<button
class="flex items-center gap-3 px-4 py-2 hover:rounded-b-md hover:bg-gray-100 dark:hover:bg-gray-700"
:class="theme === undefined ? 'text-gray-900 dark:text-gray-100' : 'text-gray-500 dark:text-gray-400'"
@click="systemMode()"
>
<?php if (isset($component)) { $__componentOriginala52e607cb40b8eec566206ff9f3ca13c = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginala52e607cb40b8eec566206ff9f3ca13c = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.computer-desktop','data' => ['class' => 'h-5 w-5']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.computer-desktop'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'h-5 w-5']); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginala52e607cb40b8eec566206ff9f3ca13c)): ?>
<?php $attributes = $__attributesOriginala52e607cb40b8eec566206ff9f3ca13c; ?>
<?php unset($__attributesOriginala52e607cb40b8eec566206ff9f3ca13c); ?>
<?php endif; ?>
<?php if (isset($__componentOriginala52e607cb40b8eec566206ff9f3ca13c)): ?>
<?php $component = $__componentOriginala52e607cb40b8eec566206ff9f3ca13c; ?>
<?php unset($__componentOriginala52e607cb40b8eec566206ff9f3ca13c); ?>
<?php endif; ?>
System
</button>
</div>
</div>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/theme-switcher.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,117 @@
<?php $__env->startSection('content'); ?>
<div class="max-w-7xl mx-auto space-y-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-2xl font-bold text-gray-800">Riwayat Analisis</h2>
<p class="text-sm text-gray-500">
Periode tersimpan otomatis setiap kali Ambil Data dijalankan.
</p>
</div>
</div>
<?php if(session('success')): ?>
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded">
<?php echo e(session('success')); ?>
</div>
<?php endif; ?>
<?php if(session('error')): ?>
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded whitespace-pre-line">
<?php echo e(session('error')); ?>
</div>
<?php endif; ?>
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold text-gray-800 mb-1">Import Data Lama dari CSV</h3>
<p class="text-sm text-gray-500 mb-4">
Gunakan untuk memasukkan ulasan historis. Kolom wajib: wisata, rating, ulasan, tanggal. Kolom reviewer boleh ada.
</p>
<form action="<?php echo e(route('riwayat.import')); ?>" method="POST" enctype="multipart/form-data"
class="grid grid-cols-1 md:grid-cols-[180px_1fr_auto] gap-3 md:items-center">
<?php echo csrf_field(); ?>
<input type="month" name="periode_bulan" value="<?php echo e(now()->format('Y-m')); ?>"
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-[150px]" required>
<input type="file" name="file" accept=".csv,.txt"
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-0" required>
<button class="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-lg text-sm shadow">
Import CSV
</button>
</form>
<?php if($errors->any()): ?>
<div class="mt-3 text-sm text-red-600">
<?php echo e($errors->first()); ?>
</div>
<?php endif; ?>
</div>
<div class="bg-white rounded-xl shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100 text-left">
<tr>
<th class="px-4 py-3">Periode</th>
<th class="px-4 py-3 text-center">Total Ulasan</th>
<th class="px-4 py-3 text-center">Hasil Analisis</th>
<th class="px-4 py-3 text-center">Wisata</th>
<th class="px-4 py-3">Terakhir Analisis</th>
<th class="px-4 py-3 text-center">Aksi</th>
</tr>
</thead>
<tbody class="divide-y">
<?php $__empty_1 = true; $__currentLoopData = $riwayat; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-semibold text-gray-800"><?php echo e($item->nama); ?></td>
<td class="px-4 py-3 text-center"><?php echo e($item->total_ulasan); ?></td>
<td class="px-4 py-3 text-center"><?php echo e($item->total_hasil); ?></td>
<td class="px-4 py-3 text-center"><?php echo e($item->total_wisata); ?></td>
<td class="px-4 py-3 text-gray-500">
<?php echo e($item->terakhir_analisis ? \Carbon\Carbon::parse($item->terakhir_analisis)->format('d M Y H:i') : '-'); ?>
</td>
<td class="px-4 py-3">
<div class="flex flex-wrap justify-center gap-2">
<a href="<?php echo e(route('dashboard', ['periode_id' => $item->id, 'tab' => 'analisis'])); ?>"
class="px-3 py-1 rounded-lg bg-green-50 text-green-600 hover:bg-green-100">
Analisis
</a>
<a href="<?php echo e(route('ulasan.index', ['periode_id' => $item->id])); ?>"
class="px-3 py-1 rounded-lg bg-gray-100 text-gray-700 hover:bg-gray-200">
Ulasan
</a>
</div>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-400">
Belum ada riwayat analisis. Jalankan Ambil Data terlebih dahulu.
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if($riwayat->hasPages()): ?>
<div class="p-4">
<?php echo e($riwayat->links()); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/ulasan/riwayat.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,110 @@
<?php if (isset($component)) { $__componentOriginalbbd4eeea836234825f7514ed20d2d52d = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginalbbd4eeea836234825f7514ed20d2d52d = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.layout','data' => ['exception' => $exception]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::layout'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['exception' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($exception)]); ?>
<div class="renderer container mx-auto lg:px-8">
<?php if (isset($component)) { $__componentOriginal10cd8b81fdad4ce00a06c99f27003014 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal10cd8b81fdad4ce00a06c99f27003014 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.navigation','data' => ['exception' => $exception]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::navigation'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['exception' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($exception)]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal10cd8b81fdad4ce00a06c99f27003014)): ?>
<?php $attributes = $__attributesOriginal10cd8b81fdad4ce00a06c99f27003014; ?>
<?php unset($__attributesOriginal10cd8b81fdad4ce00a06c99f27003014); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal10cd8b81fdad4ce00a06c99f27003014)): ?>
<?php $component = $__componentOriginal10cd8b81fdad4ce00a06c99f27003014; ?>
<?php unset($__componentOriginal10cd8b81fdad4ce00a06c99f27003014); ?>
<?php endif; ?>
<main class="px-6 pb-12 pt-6">
<div class="container mx-auto">
<?php if (isset($component)) { $__componentOriginal1e817eb3c41fe3ea9eb0c15213c4b557 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal1e817eb3c41fe3ea9eb0c15213c4b557 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.header','data' => ['exception' => $exception]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::header'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['exception' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($exception)]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal1e817eb3c41fe3ea9eb0c15213c4b557)): ?>
<?php $attributes = $__attributesOriginal1e817eb3c41fe3ea9eb0c15213c4b557; ?>
<?php unset($__attributesOriginal1e817eb3c41fe3ea9eb0c15213c4b557); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal1e817eb3c41fe3ea9eb0c15213c4b557)): ?>
<?php $component = $__componentOriginal1e817eb3c41fe3ea9eb0c15213c4b557; ?>
<?php unset($__componentOriginal1e817eb3c41fe3ea9eb0c15213c4b557); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginal1dc7d865c9b6045c4d68faf8bde572ed = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal1dc7d865c9b6045c4d68faf8bde572ed = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.trace-and-editor','data' => ['exception' => $exception]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::trace-and-editor'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['exception' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($exception)]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal1dc7d865c9b6045c4d68faf8bde572ed)): ?>
<?php $attributes = $__attributesOriginal1dc7d865c9b6045c4d68faf8bde572ed; ?>
<?php unset($__attributesOriginal1dc7d865c9b6045c4d68faf8bde572ed); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal1dc7d865c9b6045c4d68faf8bde572ed)): ?>
<?php $component = $__componentOriginal1dc7d865c9b6045c4d68faf8bde572ed; ?>
<?php unset($__componentOriginal1dc7d865c9b6045c4d68faf8bde572ed); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginal523928ff754f95aea6faf87444393a04 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal523928ff754f95aea6faf87444393a04 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.context','data' => ['exception' => $exception]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::context'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['exception' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($exception)]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal523928ff754f95aea6faf87444393a04)): ?>
<?php $attributes = $__attributesOriginal523928ff754f95aea6faf87444393a04; ?>
<?php unset($__attributesOriginal523928ff754f95aea6faf87444393a04); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal523928ff754f95aea6faf87444393a04)): ?>
<?php $component = $__componentOriginal523928ff754f95aea6faf87444393a04; ?>
<?php unset($__componentOriginal523928ff754f95aea6faf87444393a04); ?>
<?php endif; ?>
</div>
</main>
</div>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginalbbd4eeea836234825f7514ed20d2d52d)): ?>
<?php $attributes = $__attributesOriginalbbd4eeea836234825f7514ed20d2d52d; ?>
<?php unset($__attributesOriginalbbd4eeea836234825f7514ed20d2d52d); ?>
<?php endif; ?>
<?php if (isset($__componentOriginalbbd4eeea836234825f7514ed20d2d52d)): ?>
<?php $component = $__componentOriginalbbd4eeea836234825f7514ed20d2d52d; ?>
<?php unset($__componentOriginalbbd4eeea836234825f7514ed20d2d52d); ?>
<?php endif; ?>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/show.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SENTARA Dashboard</title>
<?php echo app('Illuminate\Foundation\Vite')(['resources/css/app.css','resources/js/app.js']); ?>
<!-- FEATHER ICON -->
<script src="https://unpkg.com/feather-icons"></script>
</head>
<body class="bg-gray-100 overflow-x-hidden">
<div class="min-h-screen flex">
<!-- SIDEBAR -->
<aside id="sidebar"
class="fixed top-0 left-0 h-full w-64
bg-gradient-to-b from-blue-900 to-blue-700
text-white flex flex-col
transform -translate-x-full md:translate-x-0
transition duration-300 z-50">
<!-- LOGO -->
<div class="flex justify-center mt-6 mb-8">
<img src="<?php echo e(asset('images/tulisan-sentara.png')); ?>"
class="w-44"
alt="Logo SENTARA">
</div>
<!-- MENU -->
<nav class="flex-1 px-6 space-y-3 text-sm font-medium">
<!-- DASHBOARD -->
<a href="<?php echo e(route('dashboard')); ?>"
class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-lg">
<i data-feather="grid" class="w-5 h-5"></i>
</div>
<span class="text-[15px] leading-5">
Dashboard Sentimen
</span>
</a>
<!-- DATA ULASAN -->
<a href="<?php echo e(route('ulasan.index')); ?>"
class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-lg">
<i data-feather="file-text" class="w-5 h-5"></i>
</div>
<span class="text-[15px] leading-5">
Data Ulasan
</span>
</a>
<!-- RIWAYAT -->
<a href="<?php echo e(route('riwayat.index')); ?>"
class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-lg">
<i data-feather="clock" class="w-5 h-5"></i>
</div>
<span class="text-[15px] leading-5">
Riwayat Analisis
</span>
</a>
<!-- KHUSUS ADMIN -->
<?php if(auth()->user()->role == 'admin'): ?>
<a href="<?php echo e(route('kelola-pengguna.index')); ?>"
class="flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-blue-800 transition">
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-lg">
<i data-feather="users" class="w-5 h-5"></i>
</div>
<span class="text-[15px] leading-5">
Kelola Pengguna
</span>
</a>
<?php endif; ?>
</nav>
<!-- USER -->
<div class="px-6 pb-6">
<div class="relative">
<!-- USER BUTTON -->
<button onclick="toggleUserMenu()"
class="w-full flex items-center gap-3
bg-blue-800 hover:bg-blue-900
px-4 py-3 rounded-2xl transition">
<!-- ICON -->
<div class="w-11 h-11 rounded-full bg-blue-700
flex items-center justify-center">
<i data-feather="user"
class="w-5 h-5"></i>
</div>
<!-- TEXT -->
<div class="text-left flex-1">
<p class="text-sm font-semibold capitalize">
<?php echo e(auth()->user()->name); ?>
</p>
<p class="text-xs text-blue-200 capitalize">
<?php echo e(auth()->user()->role); ?>
</p>
</div>
<!-- ARROW -->
<i data-feather="chevron-up"
class="w-4 h-4 text-blue-200"></i>
</button>
<!-- DROPDOWN -->
<div id="userMenu"
class="hidden absolute bottom-16 left-0 w-full
bg-white text-gray-700 rounded-2xl shadow-2xl overflow-hidden">
<!-- PROFILE -->
<a href="<?php echo e(route('profile')); ?>"
class="flex items-center gap-3 px-4 py-4 text-sm hover:bg-gray-100 transition">
<i data-feather="user"
class="w-4 h-4"></i>
Profile
</a>
<!-- LOGOUT -->
<form id="logoutForm"
method="POST"
action="<?php echo e(route('logout')); ?>">
<?php echo csrf_field(); ?>
<button type="submit"
class="w-full flex items-center gap-3 px-4 py-4 text-sm hover:bg-gray-100 transition text-left">
<i data-feather="log-out"
class="w-4 h-4"></i>
Logout
</button>
</form>
</div>
</div>
</div>
</aside>
<!-- CONTENT -->
<div class="flex-1 p-4 md:p-6 md:ml-64">
<!-- MOBILE BUTTON -->
<button onclick="toggleSidebar()"
class="md:hidden mb-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow">
</button>
<?php echo $__env->yieldContent('content'); ?>
</div>
</div>
<!-- MODAL LOGOUT -->
<div id="logoutModal"
class="hidden fixed inset-0 bg-black/40 flex items-center justify-center z-50 px-4">
<div class="bg-white rounded-2xl p-6 w-96 text-center shadow-2xl">
<!-- ICON -->
<div class="w-16 h-16 rounded-full
flex items-center justify-center mx-auto mb-5">
<i data-feather="log-out"
class="w-7 h-7 text-red-500"></i>
</div>
<!-- TITLE -->
<h3 class="text-xl font-bold text-gray-800 mb-2">
Konfirmasi Logout
</h3>
<!-- TEXT -->
<p class="text-sm text-gray-500 mb-6">
Apakah Anda ingin keluar dari sistem?
</p>
<!-- BUTTON -->
<div class="flex justify-center gap-3">
<button onclick="closeModal()"
class="px-5 py-2 rounded-xl bg-gray-200 hover:bg-gray-300 transition">
Tidak
</button>
<button onclick="submitLogout()"
class="px-5 py-2 rounded-xl bg-red-500 hover:bg-red-600 text-white transition">
Ya
</button>
</div>
</div>
</div>
<!-- SCRIPT -->
<script>
function toggleSidebar() {
document.getElementById('sidebar')
.classList.toggle('-translate-x-full');
}
function toggleUserMenu() {
document.getElementById('userMenu')
.classList.toggle('hidden');
}
function confirmLogout() {
document.getElementById('logoutModal')
.classList.remove('hidden');
}
function closeModal() {
document.getElementById('logoutModal')
.classList.add('hidden');
}
function submitLogout() {
document.getElementById('logoutForm').submit();
}
/* CLOSE DROPDOWN KETIKA KLIK LUAR */
window.addEventListener('click', function(e){
const button = e.target.closest('button');
const menu = document.getElementById('userMenu');
if(!e.target.closest('#userMenu') &&
!e.target.closest('[onclick="toggleUserMenu()"]')) {
menu.classList.add('hidden');
}
});
feather.replace();
</script>
</body>
</html><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/layouts/sentara.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,354 @@
<?php $__env->startSection('content'); ?>
<div class="max-w-7xl mx-auto">
<?php if(session('success')): ?>
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded">
<?php echo e(session('success')); ?>
</div>
<?php endif; ?>
<?php 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">
<?php echo e(session('error')); ?>
</div>
<?php endif; ?>
<?php if($noDataPesan ?? null): ?>
<div class="bg-blue-50 border-l-4 border-blue-400 text-blue-700 p-4 mb-6 rounded flex items-center gap-2">
<span>📅</span> <span><?php echo e($noDataPesan); ?></span>
</div>
<?php endif; ?>
<?php 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>
<?php 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: <?php echo e($periodeAktif->nama ?? 'Belum ada data'); ?>
</span>
<a href="<?php echo e(route('riwayat.index')); ?>" class="text-sm text-blue-600 hover:underline">
Lihat riwayat
</a>
</div>
<div class="flex items-center gap-3">
<?php if($periodeAktif): ?>
<span class="text-xs text-gray-400">
Terakhir update: <?php echo e($lastUpdate ? \Carbon\Carbon::parse($lastUpdate)->format('d M Y H:i') : '-'); ?>
</span>
<?php endif; ?>
<form method="GET"
action="<?php echo e(route('dashboard')); ?>"
class="flex flex-col md:flex-row gap-3 md:items-center">
<input type="month"
name="periode_bulan"
value="<?php echo e(request('periode_bulan', now()->format('Y-m'))); ?>"
max="<?php echo e(now()->format('Y-m')); ?>"
onchange="this.form.submit()"
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-[150px]">
</form>
</div>
</div>
<div x-data="{ tab: new URLSearchParams(window.location.search).get('tab') || 'dashboard' }"
x-init="$nextTick(() => { if (tab === 'dashboard') renderAllCharts() })"
class="mb-6">
<!-- TAB -->
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
<button @click="tab='dashboard'; $nextTick(() => renderAllCharts())"
: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'">
<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"><?php echo e($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"><?php echo e($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"><?php echo e($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"><?php echo e($stats['netral_persen']); ?>%</h3>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<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>
<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>
<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">
<?php $__currentLoopData = $totalPerWisata; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<div class="flex justify-between border-b pb-3">
<span><?php echo e($item->wisata); ?></span>
<span class="font-semibold text-gray-800"><?php echo e($item->total); ?> ulasan</span>
</div>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</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">
<?php echo $__env->make('analisis.index', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
</div>
<!-- ================= REKOMENDASI ================= -->
<div x-show="tab === 'rekomendasi'" id="rekomendasi-content">
<?php echo $__env->make('rekomendasi.index', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const pieData = <?php echo json_encode($chartSentimen, 15, 512) ?>;
const destinationData = <?php echo json_encode($chartDestinasi, 15, 512) ?>;
const text = <?php echo json_encode($allText, 15, 512) ?>;
function renderAllCharts() {
// PIE CHART
const pieCanvas = document.getElementById('pieChart');
if (pieCanvas) {
if (window.pieChartInstance) {
window.pieChartInstance.destroy();
window.pieChartInstance = null;
}
// Reset canvas dengan clone agar benar-benar bersih
const newPie = pieCanvas.cloneNode(false);
pieCanvas.parentNode.replaceChild(newPie, pieCanvas);
window.pieChartInstance = new Chart(newPie, { // <-- newPie, bukan pieCanvas
type: 'pie',
data: {
labels: pieData.map(i => i.sentimen),
datasets: [{
data: pieData.map(i => Number(i.total)),
backgroundColor: ['#3B82F6', '#EF4444', '#F59E0B']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false
}
});
}
// BAR CHART
const destinationCanvas = document.getElementById('destinationChart');
if (destinationCanvas) {
if (window.destinationChartInstance) {
window.destinationChartInstance.destroy();
window.destinationChartInstance = null;
}
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
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>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/dashboard.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,164 @@
<div class="hidden overflow-x-auto sm:col-span-1 lg:block">
<div
class="h-[35.5rem] scrollbar-hidden trace text-sm text-gray-400 dark:text-gray-300"
>
<div class="mb-2 inline-block rounded-full bg-red-500/20 px-3 py-2 dark:bg-red-500/20 sm:col-span-1">
<button
@click="includeVendorFrames = !includeVendorFrames"
class="inline-flex items-center font-bold leading-5 text-red-500"
>
<span x-show="includeVendorFrames">Collapse</span>
<span
x-cloak
x-show="!includeVendorFrames"
>Expand</span
>
<span class="ml-1">vendor frames</span>
<div class="flex flex-col ml-1 -mt-2" x-cloak x-show="includeVendorFrames">
<?php if (isset($component)) { $__componentOriginal707ceba27255eae48fdb0f3529710ddf = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal707ceba27255eae48fdb0f3529710ddf = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.chevron-down','data' => []] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.chevron-down'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes([]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal707ceba27255eae48fdb0f3529710ddf)): ?>
<?php $attributes = $__attributesOriginal707ceba27255eae48fdb0f3529710ddf; ?>
<?php unset($__attributesOriginal707ceba27255eae48fdb0f3529710ddf); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal707ceba27255eae48fdb0f3529710ddf)): ?>
<?php $component = $__componentOriginal707ceba27255eae48fdb0f3529710ddf; ?>
<?php unset($__componentOriginal707ceba27255eae48fdb0f3529710ddf); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginal14b1cc5db95fcca4a0f06445821cff39 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal14b1cc5db95fcca4a0f06445821cff39 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.chevron-up','data' => []] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.chevron-up'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes([]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal14b1cc5db95fcca4a0f06445821cff39)): ?>
<?php $attributes = $__attributesOriginal14b1cc5db95fcca4a0f06445821cff39; ?>
<?php unset($__attributesOriginal14b1cc5db95fcca4a0f06445821cff39); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal14b1cc5db95fcca4a0f06445821cff39)): ?>
<?php $component = $__componentOriginal14b1cc5db95fcca4a0f06445821cff39; ?>
<?php unset($__componentOriginal14b1cc5db95fcca4a0f06445821cff39); ?>
<?php endif; ?>
</div>
<div class="flex flex-col ml-1 -mt-2" x-cloak x-show="! includeVendorFrames">
<?php if (isset($component)) { $__componentOriginal14b1cc5db95fcca4a0f06445821cff39 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal14b1cc5db95fcca4a0f06445821cff39 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.chevron-up','data' => []] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.chevron-up'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes([]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal14b1cc5db95fcca4a0f06445821cff39)): ?>
<?php $attributes = $__attributesOriginal14b1cc5db95fcca4a0f06445821cff39; ?>
<?php unset($__attributesOriginal14b1cc5db95fcca4a0f06445821cff39); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal14b1cc5db95fcca4a0f06445821cff39)): ?>
<?php $component = $__componentOriginal14b1cc5db95fcca4a0f06445821cff39; ?>
<?php unset($__componentOriginal14b1cc5db95fcca4a0f06445821cff39); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginal707ceba27255eae48fdb0f3529710ddf = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal707ceba27255eae48fdb0f3529710ddf = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.icons.chevron-down','data' => []] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::icons.chevron-down'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes([]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal707ceba27255eae48fdb0f3529710ddf)): ?>
<?php $attributes = $__attributesOriginal707ceba27255eae48fdb0f3529710ddf; ?>
<?php unset($__attributesOriginal707ceba27255eae48fdb0f3529710ddf); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal707ceba27255eae48fdb0f3529710ddf)): ?>
<?php $component = $__componentOriginal707ceba27255eae48fdb0f3529710ddf; ?>
<?php unset($__componentOriginal707ceba27255eae48fdb0f3529710ddf); ?>
<?php endif; ?>
</div>
</button>
</div>
<div class="mb-12 space-y-2">
<?php $__currentLoopData = $exception->frames(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $frame): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if(! $frame->isFromVendor()): ?>
<?php
$vendorFramesCollapsed = $exception->frames()->take($loop->index)->reverse()->takeUntil(fn ($frame) => ! $frame->isFromVendor());
?>
<div x-show="! includeVendorFrames">
<?php if($vendorFramesCollapsed->isNotEmpty()): ?>
<div class="text-gray-500">
<?php echo e($vendorFramesCollapsed->count()); ?> vendor frame<?php echo e($vendorFramesCollapsed->count() > 1 ? 's' : ''); ?> collapsed
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<button
class="w-full text-left dark:border-gray-900"
x-show="<?php echo e($frame->isFromVendor() ? 'includeVendorFrames' : 'true'); ?>"
@click="index = <?php echo e($loop->index); ?>"
>
<div
x-bind:class="
index === <?php echo e($loop->index); ?>
? 'rounded-r-md bg-gray-100 dark:bg-gray-800 border-l dark:border dark:border-gray-700 border-l-red-500 dark:border-l-red-500'
: 'hover:bg-gray-100/75 dark:hover:bg-gray-800/75'
"
>
<div class="scrollbar-hidden overflow-x-auto border-l-2 border-transparent p-2">
<div class="nowrap text-gray-900 dark:text-gray-300">
<span class="inline-flex items-baseline">
<span class="text-gray-900 dark:text-gray-300"><?php echo e($frame->source()); ?></span>
<span class="font-mono text-xs">:<?php echo e($frame->line()); ?></span>
</span>
</div>
<div class="text-gray-500 dark:text-gray-400">
<?php echo e($exception->frames()->get($loop->index + 1)?->callable()); ?>
</div>
</div>
</div>
</button>
<?php if(! $frame->isFromVendor() && $exception->frames()->slice($loop->index + 1)->filter(fn ($frame) => ! $frame->isFromVendor())->isEmpty()): ?>
<?php if($exception->frames()->slice($loop->index + 1)->count()): ?>
<div x-show="! includeVendorFrames">
<div class="text-gray-500">
<?php echo e($exception->frames()->slice($loop->index + 1)->count()); ?> vendor
frame<?php echo e($exception->frames()->slice($loop->index + 1)->count() > 1 ? 's' : ''); ?> collapsed
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</div>
</div>
</div>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/trace.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,8 @@
<section
<?php echo e($attributes->merge(['class' => "@container flex flex-col p-6 sm:p-12 bg-white dark:bg-gray-900/80 text-gray-900 dark:text-gray-100 rounded-lg default:col-span-full default:lg:col-span-6 default:row-span-1 dark:ring-1 dark:ring-gray-800 shadow-xl"])); ?>
>
<?php echo e($slot); ?>
</section>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/card.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - SENTARA</title>
<link rel="icon" type="image/png" href="<?php echo e(asset('favicon.png')); ?>">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*{
margin:0;
padding:0;
box-sizing:border-box;
font-family:'Poppins',sans-serif;
}
body{
background:#f3f5fb;
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:40px;
}
/* CONTAINER */
.container{
width:1350px;
max-width:100%;
height:720px;
background:#fff;
border-radius:25px;
display:flex;
overflow:hidden;
box-shadow:0 30px 80px rgba(0,0,0,0.1);
}
/* LEFT */
.left{
width:50%;
padding:60px 80px;
display:flex;
flex-direction:column;
justify-content:center;
}
/* LOGO */
.logo{
text-align:center;
margin-bottom:25px;
}
.logo img{
width:200px;
}
/* TITLE */
.title{
text-align:center;
font-size:28px;
font-weight:600;
}
.title span{
color:#ff5c8d;
}
.subtitle{
text-align:center;
font-size:14px;
color:#777;
margin:12px 0 35px;
}
/* FORM */
.form-group{
margin-bottom:20px;
}
label{
font-size:13px;
font-weight:500;
}
input{
width:100%;
padding:14px;
border-radius:12px;
border:1px solid #ddd;
margin-top:6px;
font-size:14px;
}
.input-group{
position:relative;
}
.toggle{
position:absolute;
right:12px;
top:14px;
cursor:pointer;
}
.forgot{
text-align:right;
margin-top:6px;
}
.forgot a{
font-size:12px;
color:#2d5be3;
text-decoration:none;
}
/* BUTTON */
.btn{
width:100%;
padding:14px;
border:none;
border-radius:12px;
background:linear-gradient(90deg,#2d5be3,#4c7dff);
color:#fff;
font-weight:500;
margin-top:20px;
cursor:pointer;
transition:0.3s;
}
.btn:hover{
opacity:0.9;
}
.right{
width:50%;
position:relative;
overflow:hidden;
}
/* GAMBAR */
.right img{
position:absolute;
top:0;
right:0;
width:100%;
height:100%;
object-fit:cover;
/* SHAPE DIPERBAIKI */
clip-path: ellipse(120% 100% at 100% 50%);
}
/* OVERLAY */
.overlay{
position:absolute;
width:100%;
height:100%;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
text-align:center;
color:#fff;
background:rgba(0,0,0,0.25);
padding:20px;
z-index:2; /* DI ATAS */
}
/* QUOTE */
.quote{
font-size:42px;
color:#ff5c8d;
margin-bottom:10px;
}
/* TEXT */
.overlay p{
font-size:30px;
font-weight:500;
line-height:1.5;
max-width:420px;
}
.overlay span{
color:#ff5c8d;
font-weight:600;
}
/* GARIS */
.line{
width:70px;
height:3px;
background:#fff;
border-radius:10px;
margin-top:18px;
position:relative;
}
.line::after{
content:'';
position:absolute;
width:12px;
height:12px;
border:2px solid #fff;
border-left:none;
border-top:none;
transform:rotate(-45deg);
right:-12px;
top:-5px;
}
/* MOBILE */
@media(max-width:900px){
.container{
flex-direction:column;
height:auto;
}
.left{
width:100%;
padding:40px;
}
.right{
width:100%;
height:260px;
}
.overlay p{
font-size:20px;
}
.logo img{
width:150px;
}
}
@media(max-width:900px){
.container{
flex-direction:column;
height:auto;
}
.left{
width:100%;
padding:40px;
}
.right{
width:100%;
height:260px;
}
.overlay p{
font-size:20px;
}
.logo img{
width:150px;
}
}
/* HILANGKAN ICON PASSWORD BAWAAN BROWSER */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear {
display: none;
}
input::-ms-reveal,
input::-ms-clear {
display: none;
}
</style>
</head>
<body>
<div class="container">
<!-- LEFT -->
<div class="left">
<div class="logo">
<img src="<?php echo e(asset('images/logo-sentara.png')); ?>">
</div>
<?php if($errors->any()): ?>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
Swal.fire({
icon: 'error',
title: 'Login Gagal',
text: 'Email atau password yang dimasukkan salah.',
confirmButtonColor: '#2d5be3'
});
</script>
<?php endif; ?>
<div class="title">
Selamat Datang <span>Kembali!</span>
</div>
<div class="subtitle">
Login untuk mengakses Dashboard Analisis Sentimen Wisata Jember
</div>
<form method="POST" action="<?php echo e(route('login')); ?>">
<?php echo csrf_field(); ?>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" placeholder="Masukkan email Anda" required>
</div>
<div class="form-group">
<label>Password</label>
<div class="input-group">
<input type="password" id="password" name="password" placeholder="Masukkan password Anda" required>
<span class="toggle" onclick="togglePass()">👁</span>
</div>
<div class="forgot">
<a href="<?php echo e(route('password.request')); ?>">Lupa password?</a>
</div>
</div>
<button class="btn">Login</button>
</form>
</div>
<!-- RIGHT -->
<div class="right">
<img src="<?php echo e(asset('images/papuma.jpg')); ?>" alt="bg">
<div class="overlay">
<p>
Memahami opini,<br>
meningkatkan <span>pariwisata</span><br>
Jember.
</p>
</div>
</div>
</div>
<script>
function togglePass(){
let x = document.getElementById("password");
x.type = x.type === "password" ? "text" : "password";
}
</script>
</body>
</html><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/auth/login.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,70 @@
<?php if (isset($component)) { $__componentOriginal74daf2d0a9c625ad90327a6043d15980 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal74daf2d0a9c625ad90327a6043d15980 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.card','data' => ['class' => 'mt-6 overflow-x-auto']] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::card'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['class' => 'mt-6 overflow-x-auto']); ?>
<div
x-data="{
includeVendorFrames: false,
index: <?php echo e($exception->defaultFrame()); ?>,
}"
>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3" x-clock>
<?php if (isset($component)) { $__componentOriginal92c1a431b4816bac5d5a20d0fc1238ab = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal92c1a431b4816bac5d5a20d0fc1238ab = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.trace','data' => ['exception' => $exception]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::trace'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['exception' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($exception)]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal92c1a431b4816bac5d5a20d0fc1238ab)): ?>
<?php $attributes = $__attributesOriginal92c1a431b4816bac5d5a20d0fc1238ab; ?>
<?php unset($__attributesOriginal92c1a431b4816bac5d5a20d0fc1238ab); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal92c1a431b4816bac5d5a20d0fc1238ab)): ?>
<?php $component = $__componentOriginal92c1a431b4816bac5d5a20d0fc1238ab; ?>
<?php unset($__componentOriginal92c1a431b4816bac5d5a20d0fc1238ab); ?>
<?php endif; ?>
<?php if (isset($component)) { $__componentOriginala2de13eefed6710e7b4064d57c6d0e47 = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginala2de13eefed6710e7b4064d57c6d0e47 = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.editor','data' => ['exception' => $exception]] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::editor'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes(['exception' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute($exception)]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginala2de13eefed6710e7b4064d57c6d0e47)): ?>
<?php $attributes = $__attributesOriginala2de13eefed6710e7b4064d57c6d0e47; ?>
<?php unset($__attributesOriginala2de13eefed6710e7b4064d57c6d0e47); ?>
<?php endif; ?>
<?php if (isset($__componentOriginala2de13eefed6710e7b4064d57c6d0e47)): ?>
<?php $component = $__componentOriginala2de13eefed6710e7b4064d57c6d0e47; ?>
<?php unset($__componentOriginala2de13eefed6710e7b4064d57c6d0e47); ?>
<?php endif; ?>
</div>
</div>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $attributes = $__attributesOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__attributesOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal74daf2d0a9c625ad90327a6043d15980)): ?>
<?php $component = $__componentOriginal74daf2d0a9c625ad90327a6043d15980; ?>
<?php unset($__componentOriginal74daf2d0a9c625ad90327a6043d15980); ?>
<?php endif; ?>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/trace-and-editor.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,204 @@
<?php $__env->startSection('content'); ?>
<div class="max-w-7xl mx-auto px-4 md:px-0 space-y-6">
<!-- HEADER -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-gray-800">Data Ulasan</h2>
<p class="text-sm text-gray-500">
Periode aktif: <?php echo e($periodeAktif->nama ?? 'Belum ada data'); ?>
<a href="<?php echo e(route('riwayat.index')); ?>" class="text-blue-600 hover:underline ml-2">Lihat riwayat</a>
</p>
</div>
<div class="flex flex-col md:flex-row gap-3 md:items-center">
<form action="<?php echo e(route('ulasan.ambil')); ?>" method="POST" class="flex flex-col md:flex-row gap-3 md:items-center">
<?php echo csrf_field(); ?>
<input type="hidden" name="redirect_to" value="ulasan">
<input type="month" name="periode_bulan" value="<?php echo e($periodeAktif
? $periodeAktif->tahun . '-' . str_pad($periodeAktif->bulan, 2, '0', STR_PAD_LEFT)
: 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>
<?php $__currentLoopData = ($scraperDestinations ?? []); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $scraperDestination): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option value="<?php echo e($scraperDestination); ?>"><?php echo e($scraperDestination); ?></option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</select>
<button class="border border-blue-500 text-blue-600 px-5 py-2.5 rounded-full hover:bg-blue-50 transition">
Ambil Data
</button>
</form>
<form action="<?php echo e(route('proses.analisis')); ?>" method="POST">
<?php echo csrf_field(); ?>
<input type="hidden" name="periode_id" value="<?php echo e($periodeId); ?>">
<button
<?php echo e($totalUlasan <= 0 ? 'disabled' : ''); ?>
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded-full shadow-md transition disabled:opacity-50 disabled:cursor-not-allowed">
Proses Analisis
</button>
</form>
</div>
</div>
</div>
<!-- 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">
<div class="flex items-center gap-3">
<div class="bg-blue-100 text-blue-600 p-3 rounded-full">📄</div>
<div>
<p class="text-xs text-gray-500">Total Ulasan</p>
<p class="font-bold text-lg"><?php echo e($totalUlasan); ?></p>
</div>
</div>
<div class="flex items-center gap-3">
<div class="bg-green-100 text-green-600 p-3 rounded-full">📍</div>
<div>
<p class="text-xs text-gray-500">Total Wisata</p>
<p class="font-bold text-lg"><?php echo e($totalWisata); ?></p>
</div>
</div>
<div class="flex items-center gap-3">
<div class="bg-orange-100 text-orange-600 p-3 rounded-full">📅</div>
<div>
<p class="text-xs text-gray-500">Terakhir Update</p>
<p class="text-sm font-semibold">
<?php echo e($lastUpdate ? \Carbon\Carbon::parse($lastUpdate)->format('d M Y H:i') : '-'); ?>
</p>
</div>
</div>
</div>
</div>
<!-- ALERT -->
<?php if(session('success')): ?>
<div class="bg-green-100 text-green-700 px-4 py-2 rounded-lg text-sm">
<?php echo e(session('success')); ?>
</div>
<?php endif; ?>
<?php if(session('error')): ?>
<div class="bg-red-100 text-red-700 px-4 py-2 rounded-lg text-sm whitespace-pre-line">
<?php echo e(session('error')); ?>
</div>
<?php endif; ?>
<!-- SEARCH -->
<form method="GET" action="<?php echo e(route('ulasan.index')); ?>" class="flex flex-col md:flex-row gap-3 items-center">
<?php if(request('periode_id')): ?>
<input type="hidden" name="periode_id" value="<?php echo e(request('periode_id')); ?>">
<?php endif; ?>
<input type="text" name="search" value="<?php echo e(request('search')); ?>"
placeholder="Cari ulasan atau wisata..."
class="flex-1 border px-3 py-2 rounded-lg text-sm">
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm">Cari</button>
<button type="button"
onclick="if (confirm('Reset akan mengosongkan semua ulasan dan hasil analisis untuk periode ini. Lanjutkan?')) document.getElementById('resetPeriodeForm').submit();"
class="bg-gray-200 px-4 py-2 rounded-lg text-sm">
Reset
</button>
</form>
<?php if($periodeId): ?>
<form id="resetPeriodeForm" action="<?php echo e(route('ulasan.kosongkan-periode')); ?>" method="POST" class="hidden">
<?php echo csrf_field(); ?>
<?php echo method_field('DELETE'); ?>
<input type="hidden" name="periode_id" value="<?php echo e($periodeId); ?>">
</form>
<?php endif; ?>
<!-- TABLE -->
<div class="bg-white rounded-xl shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-3 w-[5%]">No</th>
<th class="px-4 py-3 w-[20%]">Nama Wisata</th>
<th class="px-4 py-3 w-[10%] text-center">Rating</th>
<th class="px-4 py-3 w-[45%]">Ulasan</th>
<th class="px-4 py-3 w-[20%] text-center">Tanggal</th>
</tr>
</thead>
<tbody class="divide-y">
<?php $__empty_1 = true; $__currentLoopData = $mentah; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $i => $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<tr class="hover:bg-gray-50">
<td class="px-4 py-3"><?php echo e($mentah->firstItem() + $i); ?></td>
<td class="px-4 py-3 font-medium"><?php echo e($item->wisata); ?></td>
<td class="px-4 py-3 text-center text-yellow-500">
<?php echo e(str_repeat('★', round($item->rating))); ?>
<span class="text-gray-500 ml-1"><?php echo e($item->rating); ?></span>
</td>
<td class="px-4 py-3">
<div class="line-clamp-2 text-gray-600"><?php echo e($item->ulasan); ?></div>
</td>
<td class="px-4 py-3 text-center text-gray-500">
<?php
try {
$tanggalUlasan = $item->tanggal
? \Carbon\Carbon::parse($item->tanggal)->format('d M Y')
: '-';
} catch (\Throwable $e) {
$tanggalUlasan = $item->tanggal ?: '-';
}
?>
<?php echo e($tanggalUlasan); ?>
</td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<tr>
<td colspan="5" class="text-center py-6 text-gray-400">Belum ada data</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="p-4 flex flex-col md:flex-row gap-3 justify-between items-center text-sm text-gray-500">
<div>
Menampilkan <?php echo e($mentah->firstItem() ?? 0); ?> - <?php echo e($mentah->lastItem() ?? 0); ?>
dari <?php echo e($mentah->total()); ?> data
</div>
<div class="flex items-center gap-1">
<?php if($mentah->onFirstPage()): ?>
<span class="px-3 py-1 bg-gray-200 rounded-lg">«</span>
<?php else: ?>
<a href="<?php echo e($mentah->previousPageUrl()); ?>" class="px-3 py-1 bg-gray-100 rounded-lg">«</a>
<?php endif; ?>
<?php for($i = 1; $i <= $mentah->lastPage(); $i++): ?>
<?php if($i == $mentah->currentPage()): ?>
<span class="px-3 py-1 bg-blue-600 text-white rounded-lg"><?php echo e($i); ?></span>
<?php elseif($i <= 3 || $i > $mentah->lastPage() - 2 || abs($i - $mentah->currentPage()) <= 1): ?>
<a href="<?php echo e($mentah->url($i)); ?>" class="px-3 py-1 bg-gray-100 rounded-lg"><?php echo e($i); ?></a>
<?php elseif($i == 4 || $i == $mentah->lastPage() - 3): ?>
<span class="px-2">...</span>
<?php endif; ?>
<?php endfor; ?>
<?php if($mentah->hasMorePages()): ?>
<a href="<?php echo e($mentah->nextPageUrl()); ?>" class="px-3 py-1 bg-gray-100 rounded-lg">»</a>
<?php else: ?>
<span class="px-3 py-1 bg-gray-200 rounded-lg">»</span>
<?php endif; ?>
</div>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/ulasan/index.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,222 @@
<?php if($standalone ?? false): ?>
<?php $__env->startSection('content'); ?>
<?php endif; ?>
<?php
$totalHasilAnalisis ??= $hasil->total() ?? 0;
$jumlahKelasAnalisis ??= 0;
$evaluasiTersedia = $evaluasi && $jumlahKelasAnalisis >= 2;
?>
<div class="space-y-4">
<div class="flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold">Hasil Analisis Sentimen</h2>
<p class="text-gray-500 text-sm">
Evaluasi memakai pseudo-label dari rating/rule otomatis, bukan anotasi manual.
</p>
<?php if($totalHasilAnalisis > 0 && !$evaluasiTersedia): ?>
<p class="text-sm text-yellow-700 bg-yellow-50 border border-yellow-200 rounded-lg px-3 py-2 mt-3">
Semua hasil hanya memiliki <?php echo e($jumlahKelasAnalisis); ?> kelas sentimen. Precision, recall, F1, dan akurasi tidak dihitung karena evaluasi klasifikasi membutuhkan minimal 2 kelas. Detail hasil analisis tetap ditampilkan di bawah.
</p>
<?php endif; ?>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-white p-5 rounded-xl shadow">
<p class="text-gray-500 text-sm">Precision</p>
<h3 class="text-2xl font-bold text-blue-600">
<?php echo e($evaluasiTersedia ? number_format($evaluasi->precision ?? 0, 2) : '-'); ?>
</h3>
</div>
<div class="bg-white p-5 rounded-xl shadow">
<p class="text-gray-500 text-sm">Recall</p>
<h3 class="text-2xl font-bold text-green-600">
<?php echo e($evaluasiTersedia ? number_format($evaluasi->recall ?? 0, 2) : '-'); ?>
</h3>
</div>
<div class="bg-white p-5 rounded-xl shadow">
<p class="text-gray-500 text-sm">F1 Score</p>
<h3 class="text-2xl font-bold text-purple-600">
<?php echo e($evaluasiTersedia ? number_format($evaluasi->f1_score ?? 0, 2) : '-'); ?>
</h3>
</div>
<div class="bg-white p-5 rounded-xl shadow">
<p class="text-gray-500 text-sm">Akurasi</p>
<h3 class="text-2xl font-bold text-indigo-600">
<?php echo e($evaluasiTersedia ? number_format($evaluasi->accuracy ?? 0, 2) : '-'); ?>
</h3>
</div>
</div>
<form id="filter-form" method="GET" action="<?php echo e(route('dashboard')); ?>" class="flex gap-2 mb-4">
<input type="hidden" name="tab" value="analisis">
<input type="hidden" name="periode_id" value="<?php echo e($periodeAktif->id ?? ''); ?>">
<select
name="wisata"
class="border rounded px-3 py-2"
>
<option value="">Semua Destinasi</option>
<?php $__currentLoopData = $wisataList; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<option
value="<?php echo e($item); ?>"
<?php echo e(request('wisata') == $item ? 'selected' : ''); ?>
>
<?php echo e($item); ?>
</option>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
</select>
<button
type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded"
>
Filter
</button>
</form>
<div class="bg-white rounded-xl shadow p-4 h-fit">
<h3 class="font-semibold mb-4">Detail Hasil Analisis Sentimen</h3>
<table class="w-full text-sm">
<thead class="border-b text-left">
<tr>
<th>No</th>
<th>Wisata</th>
<th>Ulasan Asli</th>
<th>Ulasan Bersih</th>
<th>Sentimen</th>
<th>Probabilitas</th>
</tr>
</thead>
<tbody>
<?php $__empty_1 = true; $__currentLoopData = $hasil; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
<tr class="border-b">
<td><?php echo e(($hasil->currentPage() - 1) * $hasil->perPage() + $loop->iteration); ?></td>
<td><?php echo e($item->wisata); ?></td>
<td class="text-gray-500"><?php echo e(Str::limit($item->ulasan_asli ?? '-', 80)); ?></td>
<td><?php echo e(Str::limit($item->hasil_preprocessing ?? '-', 80)); ?></td>
<td>
<span class="px-2 py-1 rounded text-xs
<?php echo e(strtolower($item->sentimen) == 'positif' ? 'bg-green-100 text-green-600' : ''); ?>
<?php echo e(strtolower($item->sentimen) == 'negatif' ? 'bg-red-100 text-red-600' : ''); ?>
<?php echo e(strtolower($item->sentimen) == 'netral' ? 'bg-yellow-100 text-yellow-600' : ''); ?>
">
<?php echo e(ucfirst($item->sentimen)); ?>
</span>
</td>
<td><?php echo e(number_format($item->probabilitas ?? 0, 2)); ?></td>
</tr>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
<tr>
<td colspan="6" class="text-center py-4 text-gray-500">Tidak ada data</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php 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)));
?>
<div class="mt-4 flex items-center justify-between text-sm text-gray-600">
<p>Menampilkan <?php echo e($hasil->firstItem()); ?> - <?php echo e($hasil->lastItem()); ?> dari <?php echo e($hasil->total()); ?> data</p>
<div class="flex items-center gap-1">
<?php if($hasil->onFirstPage()): ?>
<span class="px-3 py-1 rounded border bg-gray-100 text-gray-400 cursor-not-allowed">&laquo;</span>
<?php else: ?>
<a href="<?php echo e($hasil->previousPageUrl()); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">&laquo;</a>
<?php endif; ?>
<?php if(!$window->contains(1)): ?>
<a href="<?php echo e($hasil->url(1)); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">1</a>
<?php if($window->min() > 2): ?>
<span class="px-2 py-1 text-gray-400">...</span>
<?php endif; ?>
<?php endif; ?>
<?php $__currentLoopData = $window; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $page): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if($page == $current): ?>
<span class="px-3 py-1 rounded border bg-blue-600 text-white font-semibold"><?php echo e($page); ?></span>
<?php else: ?>
<a href="<?php echo e($hasil->url($page)); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600"><?php echo e($page); ?></a>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php if(!$window->contains($last)): ?>
<?php if($window->max() < $last - 1): ?>
<span class="px-2 py-1 text-gray-400">...</span>
<?php endif; ?>
<a href="<?php echo e($hasil->url($last)); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600"><?php echo e($last); ?></a>
<?php endif; ?>
<?php if($hasil->hasMorePages()): ?>
<a href="<?php echo e($hasil->nextPageUrl()); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">&raquo;</a>
<?php else: ?>
<span class="px-3 py-1 rounded border bg-gray-100 text-gray-400 cursor-not-allowed">&raquo;</span>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php if($standalone ?? false): ?>
<?php $__env->stopSection(); ?>
<?php endif; ?>
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/analisis/index.blade.php ENDPATH**/ ?>

View File

@ -0,0 +1,49 @@
<header class="mt-3 px-5 sm:mt-10">
<div class="py-3 dark:border-gray-900 sm:py-5">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="rounded-full bg-red-500/20 p-4 dark:bg-red-500/20">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6 fill-red-500 text-gray-50 dark:text-gray-950"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
</div>
<span class="text-dark ml-3 text-2xl font-bold dark:text-white sm:text-3xl">
<?php echo e($exception->title()); ?>
</span>
</div>
<div class="flex items-center gap-3 sm:gap-6">
<?php if (isset($component)) { $__componentOriginal9b6ddd2809dd60ece07dfaf1f3ef876f = $component; } ?>
<?php if (isset($attributes)) { $__attributesOriginal9b6ddd2809dd60ece07dfaf1f3ef876f = $attributes; } ?>
<?php $component = Illuminate\View\AnonymousComponent::resolve(['view' => 'laravel-exceptions-renderer::components.theme-switcher','data' => []] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?>
<?php $component->withName('laravel-exceptions-renderer::theme-switcher'); ?>
<?php if ($component->shouldRender()): ?>
<?php $__env->startComponent($component->resolveView(), $component->data()); ?>
<?php if (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
<?php $attributes = $attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
<?php endif; ?>
<?php $component->withAttributes([]); ?>
<?php echo $__env->renderComponent(); ?>
<?php endif; ?>
<?php if (isset($__attributesOriginal9b6ddd2809dd60ece07dfaf1f3ef876f)): ?>
<?php $attributes = $__attributesOriginal9b6ddd2809dd60ece07dfaf1f3ef876f; ?>
<?php unset($__attributesOriginal9b6ddd2809dd60ece07dfaf1f3ef876f); ?>
<?php endif; ?>
<?php if (isset($__componentOriginal9b6ddd2809dd60ece07dfaf1f3ef876f)): ?>
<?php $component = $__componentOriginal9b6ddd2809dd60ece07dfaf1f3ef876f; ?>
<?php unset($__componentOriginal9b6ddd2809dd60ece07dfaf1f3ef876f); ?>
<?php endif; ?>
</div>
</div>
</div>
</header>
<?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Providers/../resources/exceptions/renderer/components/navigation.blade.php ENDPATH**/ ?>