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); } }