update menu data ulasan

This commit is contained in:
MufridaFaraDiani27 2026-04-30 00:05:40 +07:00
parent 1f653dda6c
commit 84b66d4465
5 changed files with 872 additions and 229 deletions

View File

@ -5,50 +5,111 @@
use Illuminate\Http\Request;
use App\Models\Ulasan;
use App\Models\PreprocessingData;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
class UlasanController extends Controller
{
public function index(Request $request)
{
$wisata = $request->wisata;
$query = Ulasan::query();
if ($request->filled('search')) {
$query->where('ulasan', 'LIKE', '%' . $request->search . '%');
}
if ($wisata) {
$query->where('wisata', 'LIKE', '%' . $wisata . '%');
// 🔍 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 . '%');
}
// ✅ PAGINATION 50 DATA
$mentah = $query->latest()->paginate(50)->withQueryString();
// preprocessing
// OPTIONAL
$preprocessing = PreprocessingData::latest()->paginate(30, ['*'], 'pre_page');
return view('ulasan.index', compact('mentah', 'preprocessing'));
// 🔥 RINGKASAN DATA
$totalUlasan = Ulasan::count();
$totalWisata = Ulasan::distinct('wisata')->count('wisata');
$lastUpdate = Ulasan::max('updated_at');
return view('ulasan.index', compact(
'mentah',
'preprocessing',
'totalUlasan',
'totalWisata',
'lastUpdate'
));
}
public function ambilData()
public function upload(Request $request)
{
$pythonPath = "C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
$scriptPath = base_path('scraper/scraping_pipeline.py');
$request->validate([
'file' => 'required|mimes:csv,txt,xlsx,xls|max:10240'
]);
shell_exec($pythonPath . " " . $scriptPath);
$file = $request->file('file');
$extension = strtolower($file->getClientOriginalExtension());
if (in_array($extension, ['xlsx', 'xls'])) {
$data = Excel::toArray(null, $file);
$rows = $data[0] ?? [];
unset($rows[0]);
foreach ($rows as $row) {
if (empty($row) || count($row) < 4) continue;
DB::table('ulasan')->insert([
'wisata' => $row[0] ?? null,
'rating' => isset($row[1]) ? (float) $row[1] : 0,
'ulasan' => $row[2] ?? '',
'tanggal' => isset($row[3]) ? date('Y-m-d H:i:s', strtotime($row[3])) : now(),
'sentimen' => null,
'reviewer' => 'anonymous',
'created_at' => now(),
'updated_at' => now(),
]);
}
} else {
$data = array_map(fn($line) => str_getcsv($line, ';'), file($file));
unset($data[0]);
foreach ($data as $row) {
if (empty($row) || count($row) < 4) continue;
DB::table('ulasan')->insert([
'wisata' => $row[0] ?? null,
'rating' => isset($row[1]) ? (float) $row[1] : 0,
'ulasan' => $row[2] ?? '',
'tanggal' => isset($row[3]) ? date('Y-m-d H:i:s', strtotime($row[3])) : now(),
'sentimen' => null,
'reviewer' => 'anonymous',
'created_at' => now(),
'updated_at' => now(),
]);
}
}
return redirect()->route('ulasan.index')
->with('success','Data berhasil diambil');
->with('success', 'Upload berhasil dan data masuk!');
}
public function analisisData()
{
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
$scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"';
public function analisisData()
{
$pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"';
$scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"';
$output = shell_exec($pythonPath . " " . $scriptPath . " 2>&1");
shell_exec($pythonPath . " " . $scriptPath . " 2>&1");
return redirect()->route('ulasan.index')
->with('success','Analisis berhasil');
}
return redirect()->route('ulasan.index')
->with('success', 'Analisis berhasil dijalankan');
}
}

View File

@ -8,7 +8,8 @@
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
"laravel/tinker": "^2.10.1",
"maatwebsite/excel": "^3.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",

593
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bdc6d92507d397adf1c269ed49ec424d",
"content-hash": "bd6c0a0d1d5ee815cddc3b491e5fbb8e",
"packages": [
{
"name": "brick/math",
@ -135,6 +135,162 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "composer/semver",
"version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-08-20T19:15:30+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@ -508,6 +664,67 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.19.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
},
"time": "2025-10-17T16:34:55+00:00"
},
{
"name": "fruitcake/php-cors",
"version": "v1.4.0",
@ -2019,6 +2236,272 @@
],
"time": "2026-01-15T06:54:53+00:00"
},
{
"name": "maatwebsite/excel",
"version": "3.1.68",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
"reference": "1854739267d81d38eae7d8c623caf523f30f256b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/1854739267d81d38eae7d8c623caf523f30f256b",
"reference": "1854739267d81d38eae7d8c623caf523f30f256b",
"shasum": ""
},
"require": {
"composer/semver": "^3.3",
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0||^13.0",
"php": "^7.0||^8.0",
"phpoffice/phpspreadsheet": "^1.30.0",
"psr/simple-cache": "^1.0||^2.0||^3.0"
},
"require-dev": {
"laravel/scout": "^7.0||^8.0||^9.0||^10.0||^11.0",
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0||^11.0",
"predis/predis": "^1.1"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
},
"providers": [
"Maatwebsite\\Excel\\ExcelServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Maatwebsite\\Excel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Patrick Brouwers",
"email": "patrick@spartner.nl"
}
],
"description": "Supercharged Excel exports and imports in Laravel",
"keywords": [
"PHPExcel",
"batch",
"csv",
"excel",
"export",
"import",
"laravel",
"php",
"phpspreadsheet"
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.68"
},
"funding": [
{
"url": "https://laravel-excel.com/commercial-support",
"type": "custom"
},
{
"url": "https://github.com/patrickbrouwers",
"type": "github"
}
],
"time": "2026-03-17T20:51:10+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
"reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.3"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.86",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^12.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2026-04-11T18:38:28+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "monolog/monolog",
"version": "3.10.0",
@ -2526,6 +3009,114 @@
],
"time": "2025-11-20T02:34:59+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.30.4",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9",
"reference": "02970383cc12e7bf0bc0707ea6e2e8ed23a7aec9",
"shasum": ""
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": ">=7.4.0 <8.5.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"doctrine/instantiator": "^1.5",
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
},
{
"name": "Owen Leibman"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.4"
},
"time": "2026-04-19T06:00:39+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",

View File

@ -2,271 +2,259 @@
@section('content')
<div class="max-w-6xl mx-auto space-y-8">
<div class="max-w-7xl mx-auto space-y-6">
<!-- HEADER -->
<div class="flex justify-between items-center">
<h2 class="text-3xl font-bold text-gray-800">Data Ulasan</h2>
<h2 class="text-2xl font-bold text-gray-800">Data Ulasan</h2>
<!-- ============================= -->
<!-- 🔥 UPLOAD + FORMAT + SUMMARY -->
<!-- ============================= -->
<div class="bg-white rounded-xl shadow p-6">
<div class="grid grid-cols-3 gap-6 items-center">
<!-- UPLOAD -->
<div class="border-2 border-dashed border-blue-300 rounded-xl p-6 text-center">
<div class="flex justify-center mb-3">
<div class="bg-blue-100 text-blue-600 p-3 rounded-full text-xl">
☁️
</div>
</div>
<p class="text-gray-600 text-sm mb-2">
Upload file CSV / XLSX
</p>
<p class="text-gray-400 text-xs mb-3">klik tombol di bawah</p>
<form id="uploadForm" action="{{ route('ulasan.upload') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="file" name="file" id="fileInput"
accept=".csv,.xlsx,.xls"
class="hidden">
<button type="button"
onclick="document.getElementById('fileInput').click()"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm shadow">
Pilih File
</button>
</form>
<!-- <a href="#" class="block mt-3 text-blue-500 text-xs hover:underline">
Download template CSV
</a> -->
</div>
<!-- FORMAT -->
<div class="bg-blue-50 rounded-xl p-5">
<h4 class="font-semibold text-blue-700 mb-3">
Format CSV yang Diperlukan
</h4>
<ul class="space-y-2 text-sm text-gray-700">
<li> wisata, rating, ulasan, tanggal</li>
<li> Format: YYYY-MM-DD HH:MM:SS</li>
<li> Maks 10MB</li>
</ul>
</div>
<!-- SUMMARY -->
<div class="space-y-4">
<div class="flex items-center gap-3">
<div class="bg-blue-100 text-blue-600 p-3 rounded-full">📄</div>
<div>
<p class="text-xs text-gray-500">Total Ulasan</p>
<p class="font-bold text-lg">{{ $totalUlasan }}</p>
</div>
</div>
<div class="flex items-center gap-3">
<div class="bg-green-100 text-green-600 p-3 rounded-full">📍</div>
<div>
<p class="text-xs text-gray-500">Total Wisata</p>
<p class="font-bold text-lg">{{ $totalWisata }}</p>
</div>
</div>
<div class="flex items-center gap-3">
<div class="bg-orange-100 text-orange-600 p-3 rounded-full">📅</div>
<div>
<p class="text-xs text-gray-500">Terakhir Update</p>
<p class="text-sm font-semibold">
{{ $lastUpdate ? \Carbon\Carbon::parse($lastUpdate)->format('d M Y H:i') : '-' }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- ALERT -->
@if(session('success'))
<div class="bg-green-100 text-green-700 px-4 py-2 rounded-lg text-sm">
{{ session('success') }}
</div>
@endif
<!-- ============================= -->
<!-- 🔍 SEARCH + FILTER -->
<!-- ============================= -->
<form method="GET" action="{{ route('ulasan.index') }}" class="flex gap-3">
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Cari ulasan atau wisata..."
class="flex-1 border px-3 py-2 rounded-lg text-sm">
<!-- <select name="wisata" onchange="this.form.submit()"
class="border rounded-lg px-3 py-2 text-sm">
<select id="filterWisata" class="border rounded px-3 py-2 text-sm">
<option value="">Semua Destinasi</option>
<option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
<option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
<option value="Teluk Love" {{ request('wisata')=='Teluk Love'?'selected':'' }}>Teluk Love</option>
<option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
</select>
</div>
<script>
document.getElementById("filterWisata").addEventListener("change", function () {
window.location.href = "/ulasan?wisata=" + encodeURIComponent(this.value);
});
</script>
</select> -->
<!-- BUTTON -->
<form action="{{ route('ulasan.ambil') }}" method="POST">
@csrf
<button class="bg-blue-600 text-white px-5 py-2 rounded">
Ambil Data Terbaru
<button class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm">
Cari
</button>
<a href="{{ route('ulasan.index') }}"
class="bg-gray-200 px-4 py-2 rounded-lg text-sm">
Reset
</a>
</form>
<!-- ================= TABLE ================= -->
<div class="bg-white rounded-xl shadow p-6">
<!-- ============================= -->
<!-- TABLE -->
<!-- ============================= -->
<div class="bg-white rounded-xl shadow overflow-hidden">
<h3 class="font-semibold mb-4">Data Ulasan Mentah</h3>
<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>
<div class="border rounded overflow-hidden">
<tbody class="divide-y">
<!-- HEADER -->
<table class="w-full text-sm text-left table-fixed border border-gray-200 rounded-t-lg overflow-hidden">
<thead class="bg-gray-100 text-gray-700">
<tr>
<th class="px-4 py-3 w-[15%]">Wisata</th>
<th class="px-4 py-3 w-[10%] text-center">Rating</th>
<th class="px-4 py-3 w-[60%]">Ulasan</th>
<th class="px-4 py-3 w-[15%] text-center">Tanggal</th>
</tr>
</thead>
</table>
@forelse($mentah as $i => $item)
<tr class="hover:bg-gray-50">
<!-- BODY -->
<div class="max-h-[300px] overflow-y-auto border-x border-b border-gray-200 rounded-b-lg">
<table class="w-full text-sm table-fixed">
<td class="px-4 py-3">{{ $mentah->firstItem() + $i }}</td>
<tbody class="divide-y divide-gray-200">
@foreach($mentah as $item)
<tr class="hover:bg-gray-50 transition">
<!-- WISATA -->
<td class="px-4 py-3 w-[15%] font-medium text-gray-700">
<td class="px-4 py-3 font-medium">
{{ $item->wisata }}
</td>
<!-- RATING -->
<td class="px-4 py-3 w-[10%] text-center">
{{ $item->rating }}
<td class="px-4 py-3 text-center text-yellow-500">
{{ str_repeat('★', round($item->rating)) }}
<span class="text-gray-500 ml-1">{{ $item->rating }}</span>
</td>
<!-- ULASAN -->
<td class="px-4 py-3 w-[60%] text-black-700 leading-relaxed">
<div class="line-clamp-3">
<td class="px-4 py-3">
<div class="line-clamp-2 text-gray-600">
{{ $item->ulasan }}
</div>
</td>
<!-- TANGGAL -->
<td class="px-4 py-3 w-[15%] text-center text-black-700 leading-relaxed">
<td class="px-4 py-3 text-center text-gray-500">
{{ $item->tanggal }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<!-- INFO -->
<div class="text-sm text-gray-500 mt-4 text-center">
Showing {{ $mentah->firstItem() }} to {{ $mentah->lastItem() }}
of {{ $mentah->total() }} results
</div>
<!-- PAGINATION -->
<div class="flex justify-center mt-6 items-center gap-2 text-sm">
{{-- FIRST --}}
@if ($mentah->currentPage() > 1)
<a href="{{ $mentah->url(1) }}">«</a>
@endif
{{-- PREV --}}
@if ($mentah->onFirstPage())
<span class="text-gray-400"></span>
@else
<a href="{{ $mentah->previousPageUrl() }}"></a>
@endif
@php
$start = max($mentah->currentPage() - 2, 1);
$end = min($mentah->currentPage() + 2, $mentah->lastPage());
@endphp
{{-- NUMBER DINAMIS --}}
@for ($i = $start; $i <= $end; $i++)
@if ($i == $mentah->currentPage())
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">{{ $i }}</span>
@else
<a href="{{ $mentah->url($i) }}" class="px-3 py-1 hover:underline">
{{ $i }}
</a>
@endif
@endfor
{{-- NEXT --}}
@if ($mentah->hasMorePages())
<a href="{{ $mentah->nextPageUrl() }}"></a>
@else
<span class="text-gray-400"></span>
@endif
{{-- LAST --}}
@if ($mentah->currentPage() < $mentah->lastPage())
<a href="{{ $mentah->url($mentah->lastPage()) }}">»</a>
@endif
</div>
</div>
<!-- BUTTON ANALISIS -->
<form action="{{ route('ulasan.analisis') }}" method="POST">
@csrf
<button class="bg-green-600 text-white px-5 py-2 rounded">
Proses Analisis
</button>
</form>
<!-- PREPROCESSING -->
<div class="bg-white rounded-xl shadow p-6">
<h3 class="font-semibold mb-4">Hasil Preprocessing</h3>
<div id="preprocessingTabel">
<!-- TABLE -->
<div class="max-h-[300px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100 sticky top-0">
<tr>
<th class="px-4 py-2">Wisata</th>
<th class="px-4 py-2">Ulasan Mentah</th>
<th class="px-4 py-2">Ulasan Bersih</th>
</tr>
</thead>
<tbody>
@forelse($preprocessing as $row)
<tr class="border-b">
<td class="px-4 py-2">{{ $row->wisata }}</td>
<td class="px-4 py-2">{{ $row->ulasan_asli }}</td>
<td class="px-4 py-2">{{ $row->stemming }}</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center py-4 text-gray-400">
<td colspan="5" class="text-center py-6 text-gray-400">
Belum ada data
</td>
</tr>
@endforelse
</tbody>
</table>
<div class="p-4 flex justify-between items-center text-sm text-gray-500">
<!-- KIRI (FINAL FIX) -->
<div>
Menampilkan {{ $mentah->firstItem() ?? 0 }} - {{ $mentah->lastItem() ?? 0 }}
dari {{ $mentah->total() }} data
</div>
<!-- INFO -->
<div class="text-sm text-gray-500 mt-4 text-center">
Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }}
of {{ $preprocessing->total() }} results
</div>
<!-- KANAN (PAGINATION) -->
<div class="flex items-center gap-1">
<!-- PAGINATION -->
<div class="flex justify-center mt-4 items-center gap-2 text-sm">
{{-- PREV --}}
@if ($preprocessing->onFirstPage())
<span class="text-gray-400"></span>
@if ($mentah->onFirstPage())
<span class="px-3 py-1 bg-gray-200 rounded-lg">«</span>
@else
<a href="{{ $preprocessing->previousPageUrl() }}"></a>
<a href="{{ $mentah->previousPageUrl() }}"
class="px-3 py-1 bg-gray-100 rounded-lg">«</a>
@endif
@php
$start = max($preprocessing->currentPage() - 2, 1);
$end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage());
@endphp
{{-- NUMBER --}}
@for ($i = $start; $i <= $end; $i++)
@if ($i == $preprocessing->currentPage())
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">
{{ $i }}
</span>
@else
<a href="{{ $preprocessing->url($i) }}" class="px-3 py-1 hover:underline">
{{ $i }}
</a>
@for ($i = 1; $i <= $mentah->lastPage(); $i++)
@if ($i == $mentah->currentPage())
<span class="px-3 py-1 bg-blue-600 text-white rounded-lg">{{ $i }}</span>
@elseif ($i <= 3 || $i > $mentah->lastPage() - 2 || abs($i - $mentah->currentPage()) <= 1)
<a href="{{ $mentah->url($i) }}"
class="px-3 py-1 bg-gray-100 rounded-lg">{{ $i }}</a>
@elseif ($i == 4 || $i == $mentah->lastPage() - 3)
<span class="px-2">...</span>
@endif
@endfor
{{-- NEXT --}}
@if ($preprocessing->hasMorePages())
<a href="{{ $preprocessing->nextPageUrl() }}"></a>
@if ($mentah->hasMorePages())
<a href="{{ $mentah->nextPageUrl() }}"
class="px-3 py-1 bg-gray-100 rounded-lg">»</a>
@else
<span class="text-gray-400"></span>
<span class="px-3 py-1 bg-gray-200 rounded-lg">»</span>
@endif
</div>
</div>
<script>
document.getElementById("filterWisata").addEventListener("change", function () {
let wisata = this.value;
fetch(`/ulasan?wisata=${wisata}`)
.then(res => res.text())
.then(html => {
document.querySelector("#tableContainer").innerHTML =
new DOMParser().parseFromString(html, "text/html")
.querySelector("#tableContainer").innerHTML;
});
});
</script>
</tbody>
</table>
</div>
</div>
<!-- BUTTON ANALISIS -->
<div class="flex justify-end">
<form action="{{ route('ulasan.analisis') }}" method="POST">
@csrf
<button class="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg">
Proses Analisis
</button>
</form>
</div>
</div>
<!-- AUTO SUBMIT UPLOAD -->
<script>
document.addEventListener("click", function(e) {
if(e.target.closest("#preprocessingTable a")) {
e.preventDefault();
document.addEventListener("DOMContentLoaded", function () {
const input = document.getElementById("fileInput");
const form = document.getElementById("uploadForm");
let url = e.target.closest("a").getAttribute("href");
fetch(url)
.then(res => res.text())
.then(html => {
let doc = new DOMParser().parseFromString(html, 'text/html');
let newContent = doc.querySelector("#preprocessingTable").innerHTML;
document.querySelector("#preprocessingTable").innerHTML = newContent;
});
}
input.addEventListener("change", function () {
form.submit();
});
});
</script>
@endsection

View File

@ -31,12 +31,14 @@
Route::get('/ulasan', [UlasanController::class, 'index'])->name('ulasan.index');
Route::post('/ulasan/ambil', [UlasanController::class, 'ambilData'])->name('ulasan.ambil');
Route::post('/ulasan/analisis', [UlasanController::class, 'analisisData'])->name('ulasan.analisis');
Route::post('/ulasan/upload', [UlasanController::class, 'upload'])->name('ulasan.upload');
Route::get('/hasil-analisis', [AnalisisController::class, 'index'])
->name('analisis.index');
Route::get('/rekomendasi', [RekomendasiController::class, 'index'])
->name('rekomendasi.index');;
Route::get('/', function () {
return redirect()->route('login');
});
});