731 lines
24 KiB
PHP
731 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Review;
|
|
use Barryvdh\DomPDF\Facade\Pdf;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class DashboardController extends Controller
|
|
{
|
|
/**
|
|
* ===============================
|
|
* HALAMAN DASHBOARD
|
|
* ===============================
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$data = $this->getCommonData($request);
|
|
return view('dashboard', $data);
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* HALAMAN DATA MANAGEMENT
|
|
* ===============================
|
|
*/
|
|
public function dataManagement(Request $request)
|
|
{
|
|
$data = $this->getCommonData($request);
|
|
|
|
// Tambahkan pagination untuk data management
|
|
$perPage = $request->get('per_page', 10);
|
|
$search = $request->get('search', '');
|
|
|
|
$query = Review::query();
|
|
|
|
if ($search) {
|
|
$query->where(function($q) use ($search) {
|
|
$q->where('review', 'like', "%{$search}%")
|
|
->orWhere('text_final', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Filter berdasarkan sentimen
|
|
if ($request->has('filter_sentiment') && $request->filter_sentiment != '') {
|
|
$query->where('sentiment', $request->filter_sentiment);
|
|
}
|
|
|
|
$data['reviews'] = $query->latest()->paginate($perPage);
|
|
$data['search'] = $search;
|
|
$data['filter_sentiment'] = $request->filter_sentiment;
|
|
$data['per_page'] = $perPage;
|
|
|
|
return view('data-management', $data);
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* FUNGSI AMBIL DATA UMUM
|
|
* ===============================
|
|
*/
|
|
private function getCommonData($request = null)
|
|
{
|
|
// Cek apakah tabel reviews ada
|
|
if (!Schema::hasTable('reviews')) {
|
|
return $this->getEmptyData();
|
|
}
|
|
|
|
// Filter berdasarkan tanggal jika ada
|
|
$query = Review::query();
|
|
|
|
if ($request && $request->has('start_date') && $request->has('end_date') && $request->start_date && $request->end_date) {
|
|
$query->whereBetween('created_at', [
|
|
$request->start_date . ' 00:00:00',
|
|
$request->end_date . ' 23:59:59'
|
|
]);
|
|
}
|
|
|
|
// Filter berdasarkan sentimen
|
|
if ($request && $request->has('sentiment') && $request->sentiment != '') {
|
|
$query->where('sentiment', $request->sentiment);
|
|
}
|
|
|
|
$dataset = $query->latest()->get();
|
|
|
|
// Hitung sentimen
|
|
$positif = (clone $query)->where('sentiment', 'positif')->count();
|
|
$negatif = (clone $query)->where('sentiment', 'negatif')->count();
|
|
|
|
$totalData = $positif + $negatif;
|
|
$totalSafe = $totalData > 0 ? $totalData : 1;
|
|
|
|
/**
|
|
* ===============================
|
|
* SCORE DISTRIBUTION UNTUK CHART (5 Range)
|
|
* ===============================
|
|
*/
|
|
$scoreDistribution = [
|
|
'Very Negative (-5 to -9)' => 0,
|
|
'Negative (-1 to -4)' => 0,
|
|
'Positive (1 to 4)' => 0,
|
|
'Very Positive (5 to 9)' => 0,
|
|
];
|
|
|
|
// Untuk chart line distribusi (0-100)
|
|
$scoreChartDistribution = [
|
|
'0-20' => 0,
|
|
'21-40' => 0,
|
|
'41-60' => 0,
|
|
'61-80' => 0,
|
|
'81-100' => 0,
|
|
];
|
|
|
|
/**
|
|
* ===============================
|
|
* MAINTENANCE CATEGORY
|
|
* ===============================
|
|
*/
|
|
$maintenance = [
|
|
'Login / Akses' => 0,
|
|
'Performa Sistem (Server/Lambat)' => 0,
|
|
'Fitur Pembelajaran' => 0,
|
|
'UI / Tampilan' => 0,
|
|
'Bug / Error' => 0,
|
|
'Aplikasi Mobile' => 0,
|
|
'Ujian / Exam' => 0,
|
|
];
|
|
|
|
$maintenanceDetails = [
|
|
'Login / Akses' => [],
|
|
'Performa Sistem (Server/Lambat)' => [],
|
|
'Fitur Pembelajaran' => [],
|
|
'UI / Tampilan' => [],
|
|
'Bug / Error' => [],
|
|
'Aplikasi Mobile' => [],
|
|
'Ujian / Exam' => [],
|
|
];
|
|
|
|
foreach ($dataset as $row) {
|
|
$review = strtolower($row->review ?? '');
|
|
$text = strtolower($row->steming_data ?? '');
|
|
$score = (int)($row->score ?? 0);
|
|
|
|
/**
|
|
* SCORE DISTRIBUTION
|
|
*/
|
|
if ($score <= -5) {
|
|
$scoreDistribution['Very Negative (-5 to -9)']++;
|
|
} elseif ($score <= -1) {
|
|
$scoreDistribution['Negative (-1 to -4)']++;
|
|
} elseif ($score >= 5) {
|
|
$scoreDistribution['Very Positive (5 to 9)']++;
|
|
} elseif ($score >= 1) {
|
|
$scoreDistribution['Positive (1 to 4)']++;
|
|
}
|
|
|
|
/**
|
|
* SCORE DISTRIBUTION UNTUK CHART LINE (0-100)
|
|
* Konversi score dari range -9..9 ke 0..100
|
|
*/
|
|
$normalizedScore = (($score + 9) / 18) * 100;
|
|
|
|
if ($normalizedScore <= 20) {
|
|
$scoreChartDistribution['0-20']++;
|
|
} elseif ($normalizedScore <= 40) {
|
|
$scoreChartDistribution['21-40']++;
|
|
} elseif ($normalizedScore <= 60) {
|
|
$scoreChartDistribution['41-60']++;
|
|
} elseif ($normalizedScore <= 80) {
|
|
$scoreChartDistribution['61-80']++;
|
|
} else {
|
|
$scoreChartDistribution['81-100']++;
|
|
}
|
|
|
|
/**
|
|
* MAINTENANCE DETECTION
|
|
*/
|
|
$combinedText = $text . ' ' . $review;
|
|
|
|
$keywords = [
|
|
'Login / Akses' => ['login', 'akses', 'masuk', 'akun', 'log in', 'sign in'],
|
|
'Performa Sistem (Server/Lambat)' => ['lambat', 'server', 'loading', 'lemot', 'slow', 'cepat', 'lancar', 'responsif'],
|
|
'Fitur Pembelajaran' => ['materi', 'tugas', 'belajar', 'pembelajaran', 'course', 'modul', 'video', 'konten'],
|
|
'UI / Tampilan' => ['tampilan', 'ui', 'desain', 'interface', 'ux', 'user interface', 'user experience'],
|
|
'Bug / Error' => ['error', 'bug', 'crash', 'force close', 'warning', 'galat', 'masalah'],
|
|
'Aplikasi Mobile' => ['mobile', 'android', 'ios', 'hp', 'handphone', 'app', 'aplikasi'],
|
|
'Ujian / Exam' => ['ujian', 'exam', 'test', 'quiz', 'nilai', 'skor', 'ujian online']
|
|
];
|
|
|
|
foreach ($keywords as $category => $words) {
|
|
foreach ($words as $word) {
|
|
if (str_contains($combinedText, $word)) {
|
|
$maintenance[$category]++;
|
|
if (count($maintenanceDetails[$category]) < 10) {
|
|
$maintenanceDetails[$category][] = $row->review;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CONFUSION MATRIX
|
|
*/
|
|
$cmPath = storage_path('app/public/confusion_matrix.csv');
|
|
$cm = file_exists($cmPath) ? $this->readCSV($cmPath) : [
|
|
['', 'Pred Negatif', 'Pred Positif'],
|
|
['Actual Negatif', 0, 0],
|
|
['Actual Positif', 0, 0]
|
|
];
|
|
|
|
/**
|
|
* METRICS - Disesuaikan dengan format dari Colab
|
|
*/
|
|
$metricsPath = storage_path('app/public/evaluation_metrics.csv');
|
|
$metrics = file_exists($metricsPath) ? $this->readMetrics($metricsPath) : $this->getDefaultMetrics();
|
|
|
|
/**
|
|
* FORMAT METRICS UNTUK CHART
|
|
*/
|
|
$formattedMetrics = $this->formatMetricsForChart($metrics);
|
|
|
|
return [
|
|
'dataset' => $dataset,
|
|
'positif' => $positif,
|
|
'negatif' => $negatif,
|
|
'totalData' => $totalData,
|
|
'totalSafe' => $totalSafe,
|
|
'maintenance' => $maintenance,
|
|
'maintenanceDetails' => $maintenanceDetails,
|
|
'scoreDistribution' => $scoreDistribution,
|
|
'scoreChartDistribution' => $scoreChartDistribution,
|
|
'score_0_20' => $scoreChartDistribution['0-20'],
|
|
'score_21_40' => $scoreChartDistribution['21-40'],
|
|
'score_41_60' => $scoreChartDistribution['41-60'],
|
|
'score_61_80' => $scoreChartDistribution['61-80'],
|
|
'score_81_100' => $scoreChartDistribution['81-100'],
|
|
'cm' => $cm,
|
|
'metrics' => $metrics,
|
|
'formattedMetrics' => $formattedMetrics
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Format metrics untuk chart
|
|
*/
|
|
private function formatMetricsForChart($metrics)
|
|
{
|
|
return [
|
|
'accuracy' => $metrics['accuracy'] ?? 0,
|
|
'negatif' => [
|
|
'precision' => $metrics['precision_negatif'] ?? 0,
|
|
'recall' => $metrics['recall_negatif'] ?? 0,
|
|
'f1' => $metrics['f1_negatif'] ?? 0
|
|
],
|
|
'positif' => [
|
|
'precision' => $metrics['precision_positif'] ?? 0,
|
|
'recall' => $metrics['recall_positif'] ?? 0,
|
|
'f1' => $metrics['f1_positif'] ?? 0
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get default metrics structure
|
|
*/
|
|
private function getDefaultMetrics()
|
|
{
|
|
return [
|
|
'accuracy' => 0,
|
|
'precision_negatif' => 0,
|
|
'precision_positif' => 0,
|
|
'recall_negatif' => 0,
|
|
'recall_positif' => 0,
|
|
'f1_negatif' => 0,
|
|
'f1_positif' => 0,
|
|
'macro_avg_precision' => 0,
|
|
'macro_avg_recall' => 0,
|
|
'macro_avg_f1' => 0,
|
|
'weighted_avg_precision' => 0,
|
|
'weighted_avg_recall' => 0,
|
|
'weighted_avg_f1' => 0
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get empty data structure
|
|
*/
|
|
private function getEmptyData()
|
|
{
|
|
$maintenance = [
|
|
'Login / Akses' => 0,
|
|
'Performa Sistem (Server/Lambat)' => 0,
|
|
'Fitur Pembelajaran' => 0,
|
|
'UI / Tampilan' => 0,
|
|
'Bug / Error' => 0,
|
|
'Aplikasi Mobile' => 0,
|
|
'Ujian / Exam' => 0,
|
|
];
|
|
|
|
$maintenanceDetails = [
|
|
'Login / Akses' => [],
|
|
'Performa Sistem (Server/Lambat)' => [],
|
|
'Fitur Pembelajaran' => [],
|
|
'UI / Tampilan' => [],
|
|
'Bug / Error' => [],
|
|
'Aplikasi Mobile' => [],
|
|
'Ujian / Exam' => [],
|
|
];
|
|
|
|
$scoreDistribution = [
|
|
'Very Negative (-5 to -9)' => 0,
|
|
'Negative (-1 to -4)' => 0,
|
|
'Positive (1 to 4)' => 0,
|
|
'Very Positive (5 to 9)' => 0,
|
|
];
|
|
|
|
$scoreChartDistribution = [
|
|
'0-20' => 0,
|
|
'21-40' => 0,
|
|
'41-60' => 0,
|
|
'61-80' => 0,
|
|
'81-100' => 0,
|
|
];
|
|
|
|
return [
|
|
'dataset' => collect([]),
|
|
'positif' => 0,
|
|
'negatif' => 0,
|
|
'totalData' => 0,
|
|
'totalSafe' => 1,
|
|
'maintenance' => $maintenance,
|
|
'maintenanceDetails' => $maintenanceDetails,
|
|
'scoreDistribution' => $scoreDistribution,
|
|
'scoreChartDistribution' => $scoreChartDistribution,
|
|
'score_0_20' => 0,
|
|
'score_21_40' => 0,
|
|
'score_41_60' => 0,
|
|
'score_61_80' => 0,
|
|
'score_81_100' => 0,
|
|
'cm' => [
|
|
['', 'Pred Negatif', 'Pred Positif'],
|
|
['Actual Negatif', 0, 0],
|
|
['Actual Positif', 0, 0]
|
|
],
|
|
'metrics' => $this->getDefaultMetrics(),
|
|
'formattedMetrics' => $this->formatMetricsForChart($this->getDefaultMetrics())
|
|
];
|
|
}
|
|
|
|
/**
|
|
* READ METRICS DARI CSV (disesuaikan dengan format dari Colab)
|
|
*/
|
|
private function readMetrics($path)
|
|
{
|
|
$default = $this->getDefaultMetrics();
|
|
|
|
if (!file_exists($path)) {
|
|
return $default;
|
|
}
|
|
|
|
$data = $this->readCSV($path);
|
|
$metrics = $default;
|
|
|
|
foreach ($data as $row) {
|
|
if (isset($row[0]) && isset($row[1])) {
|
|
$key = strtolower(trim($row[0]));
|
|
$value = (float) trim($row[1]);
|
|
|
|
// Mapping key dari CSV ke array metrics
|
|
if (str_contains($key, 'accuracy')) {
|
|
$metrics['accuracy'] = $value;
|
|
} elseif (str_contains($key, 'precision_negatif') || str_contains($key, 'precision negatif')) {
|
|
$metrics['precision_negatif'] = $value;
|
|
} elseif (str_contains($key, 'precision_positif') || str_contains($key, 'precision positif')) {
|
|
$metrics['precision_positif'] = $value;
|
|
} elseif (str_contains($key, 'recall_negatif') || str_contains($key, 'recall negatif')) {
|
|
$metrics['recall_negatif'] = $value;
|
|
} elseif (str_contains($key, 'recall_positif') || str_contains($key, 'recall positif')) {
|
|
$metrics['recall_positif'] = $value;
|
|
} elseif (str_contains($key, 'f1_negatif') || str_contains($key, 'f1 negatif') || str_contains($key, 'f1-score negatif')) {
|
|
$metrics['f1_negatif'] = $value;
|
|
} elseif (str_contains($key, 'f1_positif') || str_contains($key, 'f1 positif') || str_contains($key, 'f1-score positif')) {
|
|
$metrics['f1_positif'] = $value;
|
|
} elseif (str_contains($key, 'macro_avg_precision') || str_contains($key, 'macro avg precision')) {
|
|
$metrics['macro_avg_precision'] = $value;
|
|
} elseif (str_contains($key, 'macro_avg_recall') || str_contains($key, 'macro avg recall')) {
|
|
$metrics['macro_avg_recall'] = $value;
|
|
} elseif (str_contains($key, 'macro_avg_f1') || str_contains($key, 'macro avg f1')) {
|
|
$metrics['macro_avg_f1'] = $value;
|
|
} elseif (str_contains($key, 'weighted_avg_precision') || str_contains($key, 'weighted avg precision')) {
|
|
$metrics['weighted_avg_precision'] = $value;
|
|
} elseif (str_contains($key, 'weighted_avg_recall') || str_contains($key, 'weighted avg recall')) {
|
|
$metrics['weighted_avg_recall'] = $value;
|
|
} elseif (str_contains($key, 'weighted_avg_f1') || str_contains($key, 'weighted avg f1')) {
|
|
$metrics['weighted_avg_f1'] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $metrics;
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* UPLOAD CSV → INSERT DATABASE
|
|
* ===============================
|
|
*/
|
|
public function upload(Request $request)
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'dataset' => 'required|mimes:csv,txt|max:10240',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return back()->withErrors($validator)->withInput();
|
|
}
|
|
|
|
$file = $request->file('dataset');
|
|
$path = $file->getRealPath();
|
|
|
|
try {
|
|
$rows = $this->readCSV($path);
|
|
|
|
if (count($rows) <= 1) {
|
|
return back()->with('error', 'File CSV tidak memiliki data!');
|
|
}
|
|
|
|
$header = array_shift($rows); // Hapus header
|
|
|
|
DB::beginTransaction();
|
|
|
|
if ($request->has('replace_data') && $request->replace_data == '1') {
|
|
Review::truncate();
|
|
}
|
|
|
|
$inserted = 0;
|
|
$failed = 0;
|
|
|
|
foreach ($rows as $row) {
|
|
try {
|
|
// Validasi minimal memiliki 4 kolom
|
|
if (count($row) >= 4) {
|
|
|
|
// Parse sentiment
|
|
$sentiment = strtolower(trim($row[3] ?? ''));
|
|
// Pastikan sentiment hanya positif atau negatif
|
|
if (!in_array($sentiment, ['positif', 'negatif'])) {
|
|
$sentiment = $sentiment == 'positive' ? 'positif' : ($sentiment == 'negative' ? 'negatif' : 'netral');
|
|
}
|
|
|
|
// Parse score
|
|
$score = is_numeric($row[2] ?? '') ? (int)$row[2] : 0;
|
|
|
|
Review::create([
|
|
'review' => $row[0] ?? '',
|
|
'steming_data' => $row[1] ?? '',
|
|
'score' => $score,
|
|
'sentiment' => $sentiment,
|
|
'created_at' => $row[4] ?? now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$inserted++;
|
|
} else {
|
|
$failed++;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$failed++;
|
|
// \Log::warning('Gagal insert row: ' . json_encode($row) . ' Error: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
$message = "Berhasil mengupload {$inserted} data";
|
|
if ($failed > 0) {
|
|
$message .= ", {$failed} data gagal diproses";
|
|
}
|
|
|
|
return back()->with('success', $message);
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return back()->with('error', 'Gagal upload: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* HAPUS SEMUA DATA
|
|
* ===============================
|
|
*/
|
|
public function truncate()
|
|
{
|
|
try {
|
|
Review::truncate();
|
|
return back()->with('success', 'Semua data berhasil dihapus!');
|
|
} catch (\Exception $e) {
|
|
return back()->with('error', 'Gagal menghapus data: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* EXPORT PDF
|
|
* ===============================
|
|
*/
|
|
public function exportPDF(Request $request)
|
|
{
|
|
$data = $this->getCommonData($request);
|
|
$data['title'] = 'Laporan Analisis Sentimen';
|
|
$data['date'] = now()->format('d F Y');
|
|
// $data['user'] = auth()->user();
|
|
|
|
$pdf = Pdf::loadView('pdf.report', $data);
|
|
$pdf->setPaper('A4', 'landscape');
|
|
|
|
return $pdf->download('laporan_sentimen_' . date('Y-m-d') . '.pdf');
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* EXPORT EXCEL (CSV)
|
|
* ===============================
|
|
*/
|
|
public function exportExcel(Request $request)
|
|
{
|
|
$data = $this->getCommonData($request);
|
|
|
|
$filename = 'data_sentimen_' . date('Y-m-d') . '.csv';
|
|
|
|
$headers = [
|
|
'Content-Type' => 'text/csv',
|
|
'Content-Disposition' => "attachment; filename=\"$filename\"",
|
|
];
|
|
|
|
$callback = function() use ($data) {
|
|
$file = fopen('php://output', 'w');
|
|
|
|
// Header CSV
|
|
fputcsv($file, ['ID', 'Review', 'Steming Data', 'Score', 'Sentimen', 'Tanggal']);
|
|
|
|
// Data
|
|
foreach ($data['dataset'] as $review) {
|
|
fputcsv($file, [
|
|
$review->id,
|
|
$review->review,
|
|
$review->steming_data,
|
|
$review->score,
|
|
$review->sentiment,
|
|
$review->created_at->format('Y-m-d H:i:s')
|
|
]);
|
|
}
|
|
|
|
fclose($file);
|
|
};
|
|
|
|
return response()->stream($callback, 200, $headers);
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* API ENDPOINT (Untuk AJAX)
|
|
* ===============================
|
|
*/
|
|
public function getStats(Request $request)
|
|
{
|
|
$data = $this->getCommonData($request);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'positif' => $data['positif'],
|
|
'negatif' => $data['negatif'],
|
|
'total' => $data['totalData'],
|
|
'positif_percentage' => $data['totalData'] > 0 ? round(($data['positif'] / $data['totalData']) * 100, 1) : 0,
|
|
'negatif_percentage' => $data['totalData'] > 0 ? round(($data['negatif'] / $data['totalData']) * 100, 1) : 0,
|
|
'score_distribution' => [
|
|
'0-20' => $data['score_0_20'] ?? 0,
|
|
'21-40' => $data['score_21_40'] ?? 0,
|
|
'41-60' => $data['score_41_60'] ?? 0,
|
|
'61-80' => $data['score_61_80'] ?? 0,
|
|
'81-100' => $data['score_81_100'] ?? 0,
|
|
],
|
|
'metrics' => [
|
|
'accuracy' => $data['metrics']['accuracy'] ?? 0,
|
|
'negatif' => [
|
|
'precision' => $data['metrics']['precision_negatif'] ?? 0,
|
|
'recall' => $data['metrics']['recall_negatif'] ?? 0,
|
|
'f1' => $data['metrics']['f1_negatif'] ?? 0
|
|
],
|
|
'positif' => [
|
|
'precision' => $data['metrics']['precision_positif'] ?? 0,
|
|
'recall' => $data['metrics']['recall_positif'] ?? 0,
|
|
'f1' => $data['metrics']['f1_positif'] ?? 0
|
|
]
|
|
],
|
|
'maintenance' => $data['maintenance'],
|
|
'cm' => $data['cm']
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* HELPER READ CSV
|
|
* ===============================
|
|
*/
|
|
private function readCSV($path)
|
|
{
|
|
$data = [];
|
|
|
|
if (!file_exists($path) || !is_readable($path)) {
|
|
return $data;
|
|
}
|
|
|
|
$file = fopen($path, 'r');
|
|
|
|
if (!$file) {
|
|
return $data;
|
|
}
|
|
|
|
// Deteksi delimiter
|
|
$firstLine = fgets($file);
|
|
rewind($file);
|
|
|
|
$delimiters = [',', ';', "\t", '|'];
|
|
$delimiter = ',';
|
|
$maxCount = 0;
|
|
|
|
foreach ($delimiters as $d) {
|
|
$count = count(str_getcsv($firstLine, $d));
|
|
if ($count > $maxCount) {
|
|
$maxCount = $count;
|
|
$delimiter = $d;
|
|
}
|
|
}
|
|
|
|
// Baca CSV
|
|
while (($row = fgetcsv($file, 0, $delimiter)) !== FALSE) {
|
|
$row = array_map('trim', $row);
|
|
$data[] = $row;
|
|
}
|
|
|
|
fclose($file);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* UPDATE SINGLE REVIEW
|
|
* ===============================
|
|
*/
|
|
public function updateReview(Request $request, $id)
|
|
{
|
|
$request->validate([
|
|
'review' => 'required|string',
|
|
'sentiment' => 'required|in:positif,negatif',
|
|
'score' => 'required|integer|min:-9|max:9'
|
|
]);
|
|
|
|
try {
|
|
$review = Review::findOrFail($id);
|
|
$review->update([
|
|
'review' => $request->review,
|
|
'steming_data' => $request->steming_data ?? $request->review,
|
|
'score' => $request->score,
|
|
'sentiment' => $request->sentiment
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Review berhasil diupdate'
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Gagal update: ' . $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* DELETE SINGLE REVIEW
|
|
* ===============================
|
|
*/
|
|
public function deleteReview($id)
|
|
{
|
|
try {
|
|
$review = Review::findOrFail($id);
|
|
$review->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Review berhasil dihapus'
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Gagal hapus: ' . $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ===============================
|
|
* HALAMAN ANALYTICS KHUSUS (Optional)
|
|
* ===============================
|
|
*/
|
|
public function showAnalytics(Request $request)
|
|
{
|
|
$data = $this->getCommonData($request);
|
|
|
|
// Data tambahan untuk analytics
|
|
$data['page_title'] = 'Analytics Dashboard';
|
|
$data['date_range'] = [
|
|
'start' => $request->start_date ?? now()->subDays(30)->format('Y-m-d'),
|
|
'end' => $request->end_date ?? now()->format('Y-m-d')
|
|
];
|
|
|
|
return view('analytics', $data);
|
|
}
|
|
|
|
|
|
}
|