MIF_HomsinNIME31231582/app/Http/Controllers/DashboardController.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);
}
}