Perbaikan dashboard admin dan fitur statistik sistem

This commit is contained in:
ramzdhani11 2026-04-28 23:56:36 +07:00
parent a4d75136f7
commit 5697938fa4
22 changed files with 1443 additions and 738 deletions

View File

@ -9,28 +9,32 @@
class AdminController extends Controller
{
public function index()
private function checkAuth()
{
if (!session('admin_logged_in')) {
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
abort(403, 'Unauthorized');
}
// Ambil semua admin dengan role 'admin'
$admins = User::where('role', 'admin')->orderBy('created_at', 'desc')->get();
}
public function index()
{
$this->checkAuth();
// Ambil hanya admin
$admins = User::where('role', 'admin')
->orderBy('created_at', 'desc')
->get();
return view('Admin.manage-admin', compact('admins'));
}
public function store(Request $request)
{
if (!session('admin_logged_in')) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$this->checkAuth();
// Validate input - role tidak termasuk, otomatis 'admin'
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email',
'email' => 'required|email|max:255|unique:users,email',
'password' => 'required|string|min:8',
]);
@ -38,110 +42,109 @@ public function store(Request $request)
return response()->json(['errors' => $validator->errors()], 422);
}
// Create admin dengan role selalu 'admin', tidak dari input
$admin = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => 'admin', // Role SELALU 'admin'
'role' => 'admin',
'email_verified_at' => now()
]);
return response()->json(['success' => 'Admin berhasil ditambahkan', 'admin' => $admin]);
return response()->json([
'success' => 'Admin berhasil ditambahkan',
'admin' => $admin
]);
}
public function edit($id)
{
if (!session('admin_logged_in')) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$this->checkAuth();
$admin = User::where('id', $id)
->where('role', 'admin')
->firstOrFail();
$admin = User::findOrFail($id);
return response()->json($admin);
}
public function update(Request $request, $id)
{
if (!session('admin_logged_in')) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$this->checkAuth();
$admin = User::findOrFail($id);
// Pastikan hanya admin yang dapat diupdate
if ($admin->role !== 'admin') {
return response()->json(['error' => 'Hanya admin yang dapat dikelola'], 422);
}
$admin = User::where('id', $id)
->where('role', 'admin')
->firstOrFail();
// Validate input - role tidak termasuk, tidak boleh diubah
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email,'.$id,
'email' => 'required|email|max:255|unique:users,email,' . $id,
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
// Update hanya name dan email, role TIDAK boleh diubah
$admin->update([
'name' => $request->name,
'email' => $request->email,
// Role selalu 'admin', tidak dapat diubah
]);
if ($request->filled('password')) {
$admin->update(['password' => Hash::make($request->password)]);
$admin->update([
'password' => Hash::make($request->password)
]);
}
return response()->json(['success' => 'Admin berhasil diperbarui', 'admin' => $admin]);
return response()->json([
'success' => 'Admin berhasil diperbarui',
'admin' => $admin
]);
}
public function destroy($id)
{
if (!session('admin_logged_in')) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$this->checkAuth();
$admin = User::where('id', $id)
->where('role', 'admin')
->firstOrFail();
$admin = User::findOrFail($id);
// Pastikan hanya admin yang dihapus
if ($admin->role !== 'admin') {
return response()->json(['error' => 'Hanya admin yang dapat dihapus'], 422);
}
// Prevent deleting the currently logged in admin
if ($admin->id == session('admin_user_id')) {
return response()->json(['error' => 'Tidak dapat menghapus akun yang sedang digunakan'], 422);
return response()->json([
'error' => 'Tidak dapat menghapus akun yang sedang digunakan'
], 422);
}
$admin->delete();
return response()->json(['success' => 'Admin berhasil dihapus']);
return response()->json([
'success' => 'Admin berhasil dihapus'
]);
}
public function toggleStatus($id)
{
if (!session('admin_logged_in')) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$this->checkAuth();
$admin = User::where('id', $id)
->where('role', 'admin')
->firstOrFail();
$admin = User::findOrFail($id);
// Prevent deactivating the currently logged in admin
if ($admin->id == session('admin_user_id')) {
return response()->json(['error' => 'Tidak dapat menonaktifkan akun yang sedang digunakan'], 422);
return response()->json([
'error' => 'Tidak dapat menonaktifkan akun yang sedang digunakan'
], 422);
}
// For simplicity, we'll use email_verified_at as status indicator
if ($admin->email_verified_at) {
$admin->update(['email_verified_at' => null]);
$status = 'inactive';
} else {
$admin->update(['email_verified_at' => now()]);
$status = 'active';
}
$status = $admin->email_verified_at ? null : now();
return response()->json(['success' => "Status admin berhasil diubah menjadi $status", 'status' => $status]);
$admin->update([
'email_verified_at' => $status
]);
return response()->json([
'success' => 'Status berhasil diubah',
'status' => $status ? 'active' : 'inactive'
]);
}
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers; // ✅ bukan Admin\
use App\Http\Controllers\Controller;
use App\Models\Upload;
use App\Models\Prediction;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class AdminDashboardController extends Controller // ✅ nama class sesuai nama file
{
public function index()
{
if (!session('admin_logged_in')) {
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
}
$modelAccuracy = 92.62; // ✅ hardcode akurasi model
$total = Upload::count();
$today = Upload::whereDate('created_at', today())->count();
$trend = Upload::select(
DB::raw('DATE(created_at) as date'),
DB::raw('count(*) as total')
)
->where('created_at', '>=', now()->subDays(6))
->groupBy('date')
->orderBy('date')
->get();
$distribution = Upload::select('category', DB::raw('count(*) as total'))
->groupBy('category')
->get();
$totalAdmin = User::where('role', 'admin')->count();
return view('Admin.index', compact(
'total',
'today',
'trend',
'distribution',
'totalAdmin',
'modelAccuracy'
));
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use App\Models\Upload;
use Illuminate\Http\Request;
class ClassificationHistoryController extends Controller
{
public function index(Request $request)
{
if (!session('admin_logged_in')) {
return redirect()->route('admin.login');
}
$query = Upload::latest();
// Filter by category
if ($request->filter && $request->filter !== 'all') {
$query->where('category', $request->filter);
}
// Search
if ($request->search) {
$query->where('category', 'like', '%' . $request->search . '%');
}
$uploads = $query->paginate(10);
return view('Admin.classification-history', compact('uploads'));
}
}

View File

@ -0,0 +1,180 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Upload;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class StatistikController extends Controller
{
public function index()
{
/*
|--------------------------------------------------------------------------
| SAMAKAN DENGAN DASHBOARD
|--------------------------------------------------------------------------
| Dashboard memakai tabel Upload
| Maka Statistik juga harus memakai Upload
|--------------------------------------------------------------------------
*/
// Total klasifikasi
$totalKlasifikasi = Upload::count();
// Hari ini
$klasifikasiHariIni = Upload::whereDate(
'created_at',
Carbon::today()
)->count();
// Jumlah admin
$penggunaAktifAdmin = User::where('role', 'admin')->count();
// Akurasi model
$akurasiRataRata = 92.62;
/*
|--------------------------------------------------------------------------
| PERSENTASE KENAIKAN
|--------------------------------------------------------------------------
*/
$kemarin = Upload::whereDate(
'created_at',
Carbon::yesterday()
)->count();
$pctHariIni = $kemarin > 0
? round((($klasifikasiHariIni - $kemarin) / $kemarin) * 100, 1)
: 0;
$bulanIni = Upload::whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year)
->count();
$bulanLalu = Upload::whereMonth(
'created_at',
now()->subMonth()->month
)
->whereYear(
'created_at',
now()->subMonth()->year
)
->count();
$pctTotal = $bulanLalu > 0
? round((($bulanIni - $bulanLalu) / $bulanLalu) * 100, 1)
: 0;
/*
|--------------------------------------------------------------------------
| GRAFIK BAR 7 HARI
|--------------------------------------------------------------------------
*/
$hariLabels = [];
$hariData = [];
$namaHari = [
'Min', 'Sen', 'Sel',
'Rab', 'Kam', 'Jum', 'Sab'
];
for ($i = 6; $i >= 0; $i--) {
$tgl = Carbon::today()->subDays($i);
$hariLabels[] = $namaHari[$tgl->dayOfWeek];
$hariData[] = Upload::whereDate(
'created_at',
$tgl
)->count();
}
/*
|--------------------------------------------------------------------------
| GRAFIK LINE 12 BULAN
|--------------------------------------------------------------------------
*/
$bulanLabels = [];
$bulanData = [];
$namaBulan = [
'Jan', 'Feb', 'Mar', 'Apr',
'Mei', 'Jun', 'Jul', 'Agu',
'Sep', 'Okt', 'Nov', 'Des'
];
for ($i = 11; $i >= 0; $i--) {
$tgl = Carbon::now()
->startOfMonth()
->subMonths($i);
$bulanLabels[] = $namaBulan[$tgl->month - 1];
$bulanData[] = Upload::whereMonth(
'created_at',
$tgl->month
)
->whereYear(
'created_at',
$tgl->year
)
->count();
}
/*
|--------------------------------------------------------------------------
| PIE CHART DATASET TRAINING
|--------------------------------------------------------------------------
*/
$datasetTomat = [
'mentah' => 232,
'setengah_matang' => 402,
'matang' => 341,
];
$totalDataset = array_sum($datasetTomat);
$distribusiData = [];
foreach ($datasetTomat as $label => $jumlah) {
$distribusiData[] = [
'label' => ucfirst(str_replace('_', ' ', $label)),
'jumlah' => $jumlah,
'persentase' => round(
($jumlah / $totalDataset) * 100,
1
),
];
}
/*
|--------------------------------------------------------------------------
| RETURN VIEW
|--------------------------------------------------------------------------
*/
return view('Admin.system-statistics', compact(
'totalKlasifikasi',
'klasifikasiHariIni',
'akurasiRataRata',
'penggunaAktifAdmin',
'pctHariIni',
'pctTotal',
'hariLabels',
'hariData',
'bulanLabels',
'bulanData',
'distribusiData'
));
}
}

View File

@ -4,22 +4,20 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use App\Models\Upload;
use App\Models\Predictions;
use Intervention\Image\Facades\Image; // ✅ tambah ini
class TomatController extends Controller
{
protected $apiUrl = 'http://127.0.0.1:5000/predict';
/**
* Show upload form
*/
public function index()
{
return view('tomat.upload');
}
/**
* Handle image upload dan kirim ke Flask API
*/
public function classify(Request $request)
{
$request->validate([
@ -29,8 +27,7 @@ public function classify(Request $request)
try {
$uploadedFile = $request->file('image');
// Kirim file langsung ke Flask tanpa resize ulang di sini
// Flask sudah handle resize 256x256 sendiri
// Kirim ke Flask (file original)
$response = Http::timeout(30)
->attach(
'image',
@ -50,22 +47,58 @@ public function classify(Request $request)
return back()->with('error', 'Error dari API: ' . $errorMsg);
}
// ✅ Kompress dan simpan gambar
$filename = time() . '_' . uniqid() . '.jpg';
$savePath = storage_path('app/public/uploads/' . $filename);
// Buat folder jika belum ada
if (!file_exists(storage_path('app/public/uploads'))) {
mkdir(storage_path('app/public/uploads'), 0755, true);
}
// Kompress gambar: resize max 800px, quality 75%
Image::make($uploadedFile->getRealPath())
->resize(400, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
})
->save($savePath, 60);
$imagePath = 'uploads/' . $filename;
// ✅ Simpan ke tabel uploads
$upload = Upload::create([
'image_path' => $imagePath,
'category' => $result['prediction']['class'],
'confidence' => $result['prediction']['confidence_percentage'],
]);
// ✅ Simpan ke tabel predictions
Predictions::create([
'upload_id' => $upload->id,
'predicted_label' => $result['prediction']['class'],
'probability' => $result['prediction']['confidence'],
]);
// Simpan hasil ke session
session([
'prediction_result' => $result,
'processed_at' => now(),
'original_size' => $uploadedFile->getSize(),
'compressed_path' => $imagePath,
]);
return redirect()->route('tomat.result');
} catch (\Illuminate\Http\Client\ConnectionException $e) {
return back()->with('error', 'Tidak dapat terhubung ke API Flask. Pastikan server Python sudah berjalan (python app.py).');
return back()->with('error', 'Tidak dapat terhubung ke API Flask.');
} catch (\Exception $e) {
return back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
}
}
// ... sisa method tidak berubah
/**
* Show prediction result
*/

View File

@ -5,10 +5,15 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use App\Models\Upload;
use Intervention\Image\Facades\Image;
class UploadController extends Controller
{
protected $flaskApiUrl = 'http://127.0.0.1:5000/predict';
/**
* Display the upload page.
*
@ -20,7 +25,7 @@ public function index()
}
/**
* Handle the file upload.
* Handle the file upload and send to Flask API.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
@ -54,134 +59,190 @@ public function store(Request $request)
try {
$file = $request->file('tomato_image');
// Generate unique filename
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
// Create directory if it doesn't exist
$uploadPath = 'uploads/tomatoes';
if (!Storage::disk('public')->exists($uploadPath)) {
Storage::disk('public')->makeDirectory($uploadPath);
}
// Store the file
$path = $file->storeAs($uploadPath, $filename, 'public');
// ✅ Ganti dengan ini (kompress sebelum simpan)
$savePath = storage_path('app/public/' . $uploadPath . '/' . $filename);
// Buat folder jika belum ada
if (!file_exists(storage_path('app/public/' . $uploadPath))) {
mkdir(storage_path('app/public/' . $uploadPath), 0755, true);
}
// Kompress: resize max 800px, quality 75%
Image::make($file->getRealPath())
->resize(400, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
})
->save($savePath, 60);
$imagePath = $uploadPath . '/' . $filename;
// Simulate AI classification (replace with actual AI logic)
$classification = $this->classifyTomato($path);
$apiResponse = $this->sendToFlaskAPI($file);
// Redirect to result page with classification data
return redirect()->route('upload.result', [
'image' => $path,
'category' => $classification['category'],
'probability' => $classification['probability']
if (!$apiResponse['success']) {
return redirect()
->route('upload.index')
->withErrors(['error' => $apiResponse['error']])
->withInput();
}
$upload = Upload::create([
'image_path' => $imagePath,
'category' => $apiResponse['category'],
'confidence' => $apiResponse['confidence'],
]);
return redirect()->route('upload.result', ['id' => $upload->id])
->with('success', 'Klasifikasi berhasil diproses.');
} catch (\Exception $e) {
return redirect()
->route('upload.index')
->withErrors(['upload' => 'Terjadi kesalahan saat mengunggah file: ' . $e->getMessage()])
->withErrors(['error' => 'Terjadi kesalahan saat mengunggah file: ' . $e->getMessage()])
->withInput();
}
}
/**
* Display the classification result.
* Send image to Flask API for classification with retry mechanism.
*
* @param \Illuminate\Http\UploadedFile $file
* @return array
*/
private function sendToFlaskAPI($file)
{
$maxRetries = 2;
$retryDelay = 1000; // milliseconds
for ($attempt = 1; $attempt <= $maxRetries + 1; $attempt++) {
try {
$response = Http::timeout(30)
->attach(
'image',
file_get_contents($file->getRealPath()),
$file->getClientOriginalName()
)
->post($this->flaskApiUrl);
if ($response->failed()) {
if ($attempt < $maxRetries + 1) {
usleep($retryDelay * 1000);
continue;
}
return [
'success' => false,
'error' => 'Gagal menghubungi API klasifikasi. Status: ' . $response->status()
];
}
$result = $response->json();
// Validate API response structure
if (!isset($result['success'])) {
return [
'success' => false,
'error' => 'Format response API tidak valid'
];
}
if (!$result['success']) {
$errorMsg = $result['message'] ?? 'Prediksi gagal';
return [
'success' => false,
'error' => 'Error dari API: ' . $errorMsg
];
}
// Validate prediction structure
if (!isset($result['prediction']['class']) || !isset($result['prediction']['confidence_percentage'])) {
return [
'success' => false,
'error' => 'Format response API tidak valid'
];
}
// Extract and validate prediction data
$category = $result['prediction']['class'];
$confidence = round((float)$result['prediction']['confidence_percentage'], 2);
// Validate category
if (!in_array($category, ['matang', 'mentah', 'setengah_matang'])) {
return [
'success' => false,
'error' => 'Kategori prediksi tidak valid'
];
}
return [
'success' => true,
'category' => $category,
'confidence' => $confidence
];
} catch (\Illuminate\Http\Client\ConnectionException $e) {
if ($attempt < $maxRetries + 1) {
usleep($retryDelay * 1000);
continue;
}
return [
'success' => false,
'error' => 'Tidak dapat terhubung ke API Flask. Pastikan server Python sudah berjalan.'
];
} catch (\Exception $e) {
if ($attempt < $maxRetries + 1) {
usleep($retryDelay * 1000);
continue;
}
return [
'success' => false,
'error' => 'Terjadi kesalahan saat memproses prediksi.'
];
}
}
return [
'success' => false,
'error' => 'Gagal memproses prediksi setelah beberapa percobaan.'
];
}
/**
* Display the classification result from database.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function result(Request $request)
{
// Get parameters from query string
$imagePath = $request->get('image');
$category = $request->get('category', 'matang');
$probability = (int) $request->get('probability', 85);
$uploadId = $request->route('id') ?? $request->query('id');
// Validate inputs
if (!$imagePath) {
return redirect()->route('upload.index')->withErrors(['error' => 'Tidak ada gambar yang dianalisis.']);
if (!$uploadId) {
return redirect()->route('upload.index')
->withErrors(['error' => 'ID upload tidak valid.']);
}
// Get color classes based on category
$colors = $this->getCategoryColors($category);
// Get description based on category
$description = $this->getCategoryDescription($category, $probability);
$upload = Upload::find($uploadId);
if (!$upload) {
return redirect()->route('upload.index')
->withErrors(['error' => 'Data klasifikasi tidak ditemukan.']);
}
return view('result', [
'imagePath' => $imagePath,
'category' => $category,
'probability' => $probability,
'maturityColor' => $colors['badge'],
'progressColor' => $colors['progress'],
'description' => $description
'imagePath' => $upload->image_path,
'category' => $upload->category,
'confidence' => $upload->confidence,
'processedAt' => $upload->created_at
]);
}
/**
* Simulate tomato classification (replace with actual AI implementation).
*
* @param string $imagePath
* @return array
*/
private function classifyTomato($imagePath)
{
// Simulate AI classification with random results
// In real implementation, this would call your AI model
$categories = ['mentah', 'setengah matang', 'matang'];
$category = $categories[array_rand($categories)];
$probability = rand(75, 98); // Random probability between 75-98%
return [
'category' => $category,
'probability' => $probability
];
}
/**
* Get color classes based on maturity category.
*
* @param string $category
* @return array
*/
private function getCategoryColors($category)
{
$colors = [
'mentah' => [
'badge' => 'bg-green-500',
'progress' => 'bg-green-500'
],
'setengah matang' => [
'badge' => 'bg-yellow-500',
'progress' => 'bg-yellow-500'
],
'matang' => [
'badge' => 'bg-red-500',
'progress' => 'bg-red-500'
]
];
return $colors[$category] ?? $colors['matang'];
}
/**
* Get description based on maturity category and probability.
*
* @param string $category
* @param int $probability
* @return string
*/
private function getCategoryDescription($category, $probability)
{
$descriptions = [
'mentah' => "Tomat ini masih dalam tahap pertumbuhan awal dengan tingkat kematangan {$probability}%. Warna hijau menunjukkan bahwa tomat belum matang sempurna dan teksturnya masih keras. Direkomendasikan untuk menunggu beberapa hari agar tomat mencapai kematangan optimal.",
'setengah matang' => "Tomat ini dalam proses pematangan dengan tingkat kematangan {$probability}%. Perpaduan warna hijau dan merah menunjukkan tomat sedang transisi. Tekstur mulai melunak dan rasa mulai terasa. Ideal untuk penggunaan dalam salad atau masakan yang membutuhkan tomat sedikit masak.",
'matang' => "Tomat ini telah mencapai kematangan optimal dengan tingkat keyakinan {$probability}%. Warna merah cerah dan tekstur lembut menunjukkan tomat siap dikonsumsi. Kandungan nutrisi dan rasa manis alami telah mencapai puncaknya. Sempurna untuk dimakan langsung atau diolah dalam berbagai masakan."
];
return $descriptions[$category] ?? $descriptions['matang'];
}
/**
* Handle admin login.
*
@ -215,9 +276,8 @@ public function adminLogin(Request $request)
// Query hanya admin dengan role 'admin'
$user = \DB::table('users')
->where('email', $email)
->where('role', 'admin') // Hanya admin yang dapat login
->first();
->where('email', $email)
->first();
// Verifikasi password dan pastikan role adalah 'admin'
if ($user && \Hash::check($password, $user->password) && $user->role === 'admin') {

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Predictions extends Model
{
protected $table = 'predictions';
protected $fillable = [
'upload_id',
'predicted_label',
'probability',
];
protected $casts = [
'probability' => 'float',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function upload()
{
return $this->belongsTo(Upload::class, 'upload_id');
}
}

28
app/Models/Upload.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Upload extends Model
{
protected $table = 'uploads';
protected $fillable = [
'image_path',
'category',
'confidence',
];
protected $casts = [
'confidence' => 'float',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// Tambahkan ini
public function prediction()
{
return $this->hasOne(Prediction::class, 'upload_id');
}
}

View File

@ -7,9 +7,9 @@
"license": "MIT",
"require": {
"php": "^8.2",
"intervention/image": "2.7",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"intervention/image": "^2.7"
"laravel/tinker": "^2.10.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",

86
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": "c514d8f7b9fc5970bdd94287905ef584",
"content-hash": "1c02b53f5f6daedbd6f7bb6a63f7c353",
"packages": [
{
"name": "brick/math",
@ -1052,6 +1052,90 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
{
"name": "intervention/image",
"version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "9a8cc99d30415ec0b3f7649e1647d03a55698545"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/9a8cc99d30415ec0b3f7649e1647d03a55698545",
"reference": "9a8cc99d30415ec0b3f7649e1647d03a55698545",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"guzzlehttp/psr7": "~1.1 || ^2.0",
"php": ">=5.4.0"
},
"require-dev": {
"mockery/mockery": "~0.9.2",
"phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15"
},
"suggest": {
"ext-gd": "to use GD library based image processing.",
"ext-imagick": "to use Imagick based image processing.",
"intervention/imagecache": "Caching extension for the Intervention Image library"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Image": "Intervention\\Image\\Facades\\Image"
},
"providers": [
"Intervention\\Image\\ImageServiceProvider"
]
},
"branch-alias": {
"dev-master": "2.4-dev"
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@olivervogel.com",
"homepage": "http://olivervogel.com/"
}
],
"description": "Image handling and manipulation library with support for Laravel integration",
"homepage": "http://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"laravel",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/2.7.0"
},
"funding": [
{
"url": "https://www.paypal.me/interventionphp",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2021-10-03T14:17:12+00:00"
},
{
"name": "laravel/framework",
"version": "v12.39.0",

View File

@ -65,7 +65,7 @@
|
*/
'timezone' => 'UTC',
'timezone' => 'Asia/Jakarta',
/*
|--------------------------------------------------------------------------

View File

@ -14,7 +14,7 @@
public function up(): void
{
// Hapus semua user dengan role 'user'
DB::table('users')->where('role', 'user')->delete();
// Skip karena tidak ada kolom role
// Set default role admin untuk semua user yang tersisa
DB::table('users')->update(['role' => 'admin']);

View File

@ -11,6 +11,8 @@ public function up(): void
Schema::create('uploads', function (Blueprint $table) {
$table->id();
$table->string('image_path'); // hasil upload user
$table->string('category'); // hasil: matang / mentah / setengah_matang
$table->float('confidence', 10, 2); // nilai confidence (%)
$table->timestamps();
});
}

247
notifikasi_setup.md Normal file
View File

@ -0,0 +1,247 @@
# Setup Notifikasi Sistem Klasifikasi Tomat
## 🚨 Masalah Saat Ini
- Notifikasi menyebabkan 404 karena route tidak ada
- Redirect ke halaman yang belum dibuat
## ✅ Solusi Sederhana (SUDAH DIPERBAIKI)
- Notifikasi sekarang hanya menampilkan toast "dibaca"
- Tidak ada redirect yang menyebabkan 404
- Aman digunakan
## 🎯 Saran Implementasi Lengkap
### 1. Buat Route di `routes/web.php`
```php
// Admin Notifications
Route::get('/admin/notifications', [NotificationController::class, 'index'])->name('admin.notifications');
Route::get('/admin/classifications', [ClassificationController::class, 'index'])->name('admin.classifications');
Route::get('/admin/uploads', [UploadController::class, 'index'])->name('admin.uploads');
Route::get('/admin/model', [ModelController::class, 'index'])->name('admin.model');
Route::get('/admin/activity', [ActivityController::class, 'index'])->name('admin.activity');
```
### 2. Buat Controller
```php
// app/Http/Controllers/Admin/NotificationController.php
class NotificationController extends Controller
{
public function index()
{
$notifications = auth()->user()->notifications()->paginate(20);
return view('admin.notifications.index', compact('notifications'));
}
}
```
### 3. Buat Database Migration
```php
// create_notifications_table
Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('type'); // classification, upload, model, activity
$table->string('title');
$table->text('message');
$table->boolean('read')->default(false);
$table->timestamps();
});
```
### 4. Real-time dengan Pusher/Soketi
```javascript
// Echo/Laravel Echo
Echo.private('user.' + userId)
.notification((notification) => {
addNewNotification(notification.type, notification.title, notification.message);
});
```
### 5. Backend Notification System
```php
// Helper function
function sendNotification($userId, $type, $title, $message)
{
$notification = [
'user_id' => $userId,
'type' => $type,
'title' => $title,
'message' => $message,
'read' => false
];
Notification::create($notification);
// Broadcast real-time
broadcast(new NewNotification($notification));
}
```
## 📝 Trigger Points
### Klasifikasi Baru
```php
// Di ClassificationController
sendNotification(
auth()->id(),
'classification',
'Klasifikasi Baru',
"$count gambar tomat berhasil diklasifikasikan"
);
```
### Upload Baru
```php
// Di UploadController
sendNotification(
auth()->id(),
'upload',
'Upload Baru',
"$count gambar baru diunggah ke dataset"
);
```
### Status Model
```php
// Di ModelController setelah training
sendNotification(
auth()->id(),
'model',
'Status Model',
"Model berhasil diperbarui dengan akurasi $accuracy%"
);
```
### Aktivitas Admin
```php
// Di ActivityController
sendNotification(
$adminId,
'activity',
'Aktivitas Admin',
$activityDescription
);
```
## 🔧 Update Layout (Optional)
Jika sudah ada route, update JavaScript di `app.blade.php`:
```javascript
// Ganti handleNotificationClick function
function handleNotificationClick(type) {
const routes = {
'classification': '{{ route("admin.classifications") }}',
'upload': '{{ route("admin.uploads") }}',
'model': '{{ route("admin.model") }}',
'activity': '{{ route("admin.activity") }}'
};
window.location.href = routes[type] || '{{ route("admin.dashboard") }}';
}
```
## 📊 Dashboard Widget
Tambahkan widget notifikasi di dashboard:
```blade
<!-- Di dashboard.blade.php -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-semibold mb-4">Notifikasi Terbaru</h3>
<div class="space-y-3">
@foreach($recentNotifications as $notif)
<div class="flex items-center space-x-3 p-3 hover:bg-gray-50 rounded">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fas fa-bell text-blue-600 text-sm"></i>
</div>
<div class="flex-1">
<p class="text-sm font-medium">{{ $notif->title }}</p>
<p class="text-xs text-gray-500">{{ $notif->created_at->diffForHumans() }}</p>
</div>
</div>
@endforeach
</div>
</div>
```
## 🎨 UI Improvements
### 1. Badge Counter
```javascript
// Update badge dengan jumlah notifikasi
function updateNotificationBadge(count) {
const badge = document.getElementById('notificationBadge');
if (count > 0) {
badge.textContent = count > 99 ? '99+' : count;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
```
### 2. Sound Notification
```javascript
// Play sound untuk notifikasi baru
function playNotificationSound() {
const audio = new Audio('/sounds/notification.mp3');
audio.play().catch(e => console.log('Audio play failed:', e));
}
```
### 3. Browser Push Notification
```javascript
// Request permission dan show push notification
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('Notifikasi Baru', {
body: message,
icon: '/icon.png'
});
}
```
## 🚀 Production Setup
### 1. Queue System
```php
// Use queue untuk notifikasi
sendNotification($userId, $type, $title, $message)
->delay(now()->addSeconds(1))
->onQueue('notifications');
```
### 2. Cache Performance
```php
// Cache notifikasi untuk performance
$notifications = Cache::remember(
"user_{$userId}_notifications",
300, // 5 minutes
fn() => $user->notifications()->latest()->limit(50)->get()
);
```
### 3. Cleanup Old Notifications
```php
// Schedule cleanup
$schedule->command('notifications:cleanup')->daily();
```
## 📱 Mobile Responsiveness
```css
/* Mobile notification styles */
@media (max-width: 768px) {
#notificationDropdown {
width: 100vw;
right: 0;
left: 0;
max-height: 50vh;
}
}
```
---
## ✅ Status Saat Ini: AMAN
Notifikasi sudah bekerja dengan baik tanpa error 404. Implementasi lengkap di atas adalah opsional untuk pengembangan lebih lanjut.

View File

@ -1,226 +1,169 @@
@extends('Admin.layouts.app')
@section('title', 'Riwayat Klasifikasi')
@section('page-title', 'Riwayat Klasifikasi')
@section('content')
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Riwayat Klasifikasi</h1>
<p class="text-gray-600">Lihat semua riwayat klasifikasi tomat yang telah dilakukan</p>
</div>
<!-- Search and Filter Section -->
<!-- Search and Filter -->
<form method="GET" action="{{ route('admin.classification-history') }}">
<div class="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between mb-6">
<!-- Search Bar -->
<div class="relative flex-1 max-w-md">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search text-gray-400"></i>
</div>
<input type="text"
id="searchInput"
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Cari riwayat…"
class="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all">
</div>
<!-- Filter Buttons -->
<div class="flex flex-wrap gap-2">
<button class="filter-btn active px-4 py-2 rounded-full border border-gray-300 text-sm font-medium" data-filter="all">
<a href="{{ route('admin.classification-history') }}"
class="px-4 py-2 rounded-full border text-sm font-medium {{ !request('filter') ? 'bg-red-500 text-white border-red-500' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50' }}">
Semua
</button>
<button class="filter-btn px-4 py-2 rounded-full border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50" data-filter="mentah">
</a>
<a href="{{ route('admin.classification-history', ['filter' => 'mentah']) }}"
class="px-4 py-2 rounded-full border text-sm font-medium {{ request('filter') == 'mentah' ? 'bg-red-500 text-white border-red-500' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50' }}">
Mentah
</button>
<button class="filter-btn px-4 py-2 rounded-full border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50" data-filter="setengah-matang">
</a>
<a href="{{ route('admin.classification-history', ['filter' => 'setengah_matang']) }}"
class="px-4 py-2 rounded-full border text-sm font-medium {{ request('filter') == 'setengah_matang' ? 'bg-red-500 text-white border-red-500' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50' }}">
Setengah Matang
</button>
<button class="filter-btn px-4 py-2 rounded-full border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50" data-filter="matang">
</a>
<a href="{{ route('admin.classification-history', ['filter' => 'matang']) }}"
class="px-4 py-2 rounded-full border text-sm font-medium {{ request('filter') == 'matang' ? 'bg-red-500 text-white border-red-500' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50' }}">
Matang
</a>
<button type="submit"
class="px-4 py-2 rounded-full border text-sm font-medium bg-gray-800 text-white border-gray-800 hover:bg-gray-700">
<i class="fas fa-search mr-1"></i> Cari
</button>
</div>
</div>
</form>
<!-- Classification Table -->
<!-- Table -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Gambar
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tanggal Unggah
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Klasifikasi
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Skor Keyakinan
</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">No</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Gambar</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tanggal Upload</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Klasifikasi</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Skor Keyakinan</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<!-- Sample Data Rows -->
<tr class="table-row" data-classification="matang">
@forelse ($uploads as $index => $upload)
<tr class="hover:bg-gray-50 transition-colors">
<!-- No Urut -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ ($uploads->currentPage() - 1) * $uploads->perPage() + $index + 1 }}
</td>
<!-- Gambar -->
<td class="px-6 py-4 whitespace-nowrap">
<img src="https://picsum.photos/seed/tomato1/50/50.jpg"
<img src="{{ asset('storage/' . $upload->image_path) }}"
alt="Tomato"
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
class="w-12 h-12 rounded-lg object-cover border-2 border-gray-200">
</td>
<!-- Tanggal -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
23 Nov 2024, 14:30
{{ $upload->created_at->format('d M Y, H:i') }}
</td>
<!-- Klasifikasi -->
<td class="px-6 py-4 whitespace-nowrap">
<span class="classification-badge px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-pink-100 text-pink-800">
Matang
@php
$badgeClass = match($upload->category) {
'matang' => 'bg-pink-100 text-pink-800',
'mentah' => 'bg-green-100 text-green-800',
'setengah_matang' => 'bg-yellow-100 text-yellow-800',
default => 'bg-gray-100 text-gray-800'
};
@endphp
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full {{ $badgeClass }}">
{{ ucfirst(str_replace('_', ' ', $upload->category)) }}
</span>
</td>
<!-- Confidence -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="flex items-center">
<span class="mr-2">95.2%</span>
<div class="w-16 bg-gray-200 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full" style="width: 95.2%"></div>
<div class="flex items-center gap-2">
<span class="w-14">{{ $upload->confidence }}%</span>
<div class="w-24 bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full {{ $upload->confidence >= 80 ? 'bg-green-500' : ($upload->confidence >= 60 ? 'bg-yellow-500' : 'bg-red-400') }}"
style="width: {{ min($upload->confidence, 100) }}%"></div>
</div>
</div>
</td>
</tr>
<tr class="table-row" data-classification="setengah-matang">
<td class="px-6 py-4 whitespace-nowrap">
<img src="https://picsum.photos/seed/tomato2/50/50.jpg"
alt="Tomato"
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
23 Nov 2024, 13:45
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="classification-badge px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
Setengah Matang
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="flex items-center">
<span class="mr-2">87.8%</span>
<div class="w-16 bg-gray-200 rounded-full h-2">
<div class="bg-yellow-500 h-2 rounded-full" style="width: 87.8%"></div>
</div>
</div>
</td>
</tr>
<tr class="table-row" data-classification="mentah">
<td class="px-6 py-4 whitespace-nowrap">
<img src="https://picsum.photos/seed/tomato3/50/50.jpg"
alt="Tomato"
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
23 Nov 2024, 12:20
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="classification-badge px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Mentah
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="flex items-center">
<span class="mr-2">92.1%</span>
<div class="w-16 bg-gray-200 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full" style="width: 92.1%"></div>
</div>
</div>
</td>
</tr>
<tr class="table-row" data-classification="matang">
<td class="px-6 py-4 whitespace-nowrap">
<img src="https://picsum.photos/seed/tomato4/50/50.jpg"
alt="Tomato"
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
23 Nov 2024, 11:15
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="classification-badge px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-pink-100 text-pink-800">
Matang
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="flex items-center">
<span class="mr-2">89.5%</span>
<div class="w-16 bg-gray-200 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full" style="width: 89.5%"></div>
</div>
</div>
</td>
</tr>
<tr class="table-row" data-classification="setengah-matang">
<td class="px-6 py-4 whitespace-nowrap">
<img src="https://picsum.photos/seed/tomato5/50/50.jpg"
alt="Tomato"
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
23 Nov 2024, 10:30
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="classification-badge px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
Setengah Matang
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="flex items-center">
<span class="mr-2">91.3%</span>
<div class="w-16 bg-gray-200 rounded-full h-2">
<div class="bg-yellow-500 h-2 rounded-full" style="width: 91.3%"></div>
</div>
</div>
@empty
<tr>
<td colspan="5" class="px-6 py-8 text-center text-gray-500">
<i class="fas fa-inbox text-4xl mb-2 text-gray-300 block"></i>
<p>Belum ada data klasifikasi</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-700">
Menampilkan <span class="font-medium">1</span> hingga <span class="font-medium">5</span> dari <span class="font-medium">24</span> hasil
Menampilkan <span class="font-medium">{{ $uploads->firstItem() ?? 0 }}</span>
hingga <span class="font-medium">{{ $uploads->lastItem() ?? 0 }}</span>
dari <span class="font-medium">{{ $uploads->total() }}</span> hasil
</div>
<div class="flex items-center space-x-2">
<button class="pagination-btn px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fas fa-chevron-left"></i>
</button>
<button class="pagination-btn active px-3 py-2 text-sm font-medium border border-gray-300 rounded-lg">
1
</button>
<button class="pagination-btn px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
2
</button>
<button class="pagination-btn px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
3
</button>
<button class="pagination-btn px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
4
</button>
<button class="pagination-btn px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
5
</button>
<button class="pagination-btn px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
<i class="fas fa-chevron-right"></i>
</button>
<div class="flex items-center space-x-1">
{{-- Prev --}}
@if ($uploads->onFirstPage())
<span class="px-3 py-2 text-sm text-gray-400 bg-white border border-gray-300 rounded-lg cursor-not-allowed">
<i class="fas fa-chevron-left"></i>
</span>
@else
<a href="{{ $uploads->previousPageUrl() }}"
class="px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
<i class="fas fa-chevron-left"></i>
</a>
@endif
{{-- Page Numbers --}}
@foreach ($uploads->getUrlRange(1, $uploads->lastPage()) as $page => $url)
@if ($page == $uploads->currentPage())
<span class="px-3 py-2 text-sm font-medium text-white bg-red-500 border border-red-500 rounded-lg">
{{ $page }}
</span>
@else
<a href="{{ $url }}"
class="px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
{{ $page }}
</a>
@endif
@endforeach
{{-- Next --}}
@if ($uploads->hasMorePages())
<a href="{{ $uploads->nextPageUrl() }}"
class="px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
<i class="fas fa-chevron-right"></i>
</a>
@else
<span class="px-3 py-2 text-sm text-gray-400 bg-white border border-gray-300 rounded-lg cursor-not-allowed">
<i class="fas fa-chevron-right"></i>
</span>
@endif
</div>
</div>
</div>

View File

@ -1,181 +1,193 @@
@extends('Admin.layouts.app')
@section('title', 'Dashboard')
@section('page-title', 'Dashboard')
@section('content')
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Dashboard</h1>
<p class="text-gray-600">Selamat datang di dashboard sistem klasifikasi tomat</p>
<p class="text-gray-600">
Selamat datang di dashboard sistem klasifikasi tomat
</p>
</div>
<!-- Summary Cards -->
<!-- CARD -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Total Klasifikasi Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
<i class="fas fa-chart-bar text-blue-600 text-xl"></i>
</div>
<span class="text-xs text-green-600 font-medium bg-green-50 px-2 py-1 rounded-full">
+12.5%
</span>
<!-- Total -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-chart-bar text-blue-600 text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-1">12,500</h3>
<p class="text-sm text-gray-600">Total Klasifikasi</p>
<h3 class="text-2xl font-bold text-gray-900">{{ $total }}</h3>
<p class="text-sm text-gray-600 mt-2">Total Klasifikasi</p>
</div>
<!-- Akurasi Sistem Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
<i class="fas fa-bullseye text-purple-600 text-xl"></i>
</div>
<span class="text-xs text-green-600 font-medium bg-green-50 px-2 py-1 rounded-full">
+2.1%
</span>
<!-- Akurasi -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-bullseye text-purple-600 text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-1">95.2%</h3>
<p class="text-sm text-gray-600">Akurasi Sistem</p>
<h3 class="text-2xl font-bold text-gray-900">{{ $modelAccuracy }}%</h3>
<p class="text-sm text-gray-600 mt-2">Akurasi Model</p>
</div>
<!-- Admin Aktif Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center">
<i class="fas fa-users text-orange-600 text-xl"></i>
</div>
<span class="text-xs bg-orange-100 text-orange-800 font-medium px-3 py-1 rounded-full">
{{ App\Models\User::where('role', 'admin')->count() }}
</span>
<!-- Admin -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-users text-orange-600 text-xl"></i>
</div>
<h3 class="text-lg font-bold text-gray-900 mb-1">Admin Aktif</h3>
<p class="text-sm text-gray-600">Total admin terdaftar</p>
<h3 class="text-2xl font-bold text-gray-900">{{ $totalAdmin }}</h3>
<p class="text-sm text-gray-600 mt-2">Admin Aktif</p>
</div>
<!-- Data Hari Ini Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
<i class="fas fa-calendar-day text-green-600 text-xl"></i>
</div>
<span class="text-xs text-green-600 font-medium bg-green-50 px-2 py-1 rounded-full">
+8.2%
</span>
<!-- Hari Ini -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mb-4">
<i class="fas fa-calendar-day text-green-600 text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-1">230</h3>
<p class="text-sm text-gray-600">Data Hari Ini</p>
<h3 class="text-2xl font-bold text-gray-900">{{ $today }}</h3>
<p class="text-sm text-gray-600 mt-2">Klasifikasi Hari Ini</p>
</div>
</div>
<!-- Charts Section -->
<!-- CHART -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Trend Chart -->
<div class="chart-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">Trend Klasifikasi</h2>
<p class="text-sm text-gray-600">Jumlah klasifikasi per hari dalam seminggu terakhir</p>
</div>
<div class="chart-container">
<!-- Trend -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">
Trend Klasifikasi
</h2>
<p class="text-sm text-gray-600 mb-6">
Jumlah klasifikasi dalam 7 hari terakhir
</p>
<div style="height:320px">
<canvas id="trendChart"></canvas>
</div>
</div>
<!-- Classification Distribution -->
<div class="chart-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">Distribusi Klasifikasi</h2>
<p class="text-sm text-gray-600">Persentase kategori kematangan tomat</p>
</div>
<div class="chart-container">
<!-- Pie -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">
Distribusi Klasifikasi
</h2>
<p class="text-sm text-gray-600 mb-6">
Persentase kategori kematangan tomat
</p>
<div style="height:320px">
<canvas id="distributionChart"></canvas>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
// Trend Chart
const trendCtx = document.getElementById('trendChart').getContext('2d');
new Chart(trendCtx, {
type: 'line',
data: {
labels: ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'],
datasets: [{
label: 'Jumlah Klasifikasi',
data: [180, 220, 195, 240, 210, 185, 230],
borderColor: 'rgba(239, 68, 68, 1)',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointBackgroundColor: 'rgba(239, 68, 68, 1)',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 6,
pointHoverRadius: 8
}]
/* =========================
TREND LINE
========================= */
const trendLabels = @json(
$trend->pluck('date')->map(fn($d) =>
\Carbon\Carbon::parse($d)->translatedFormat('D')
)
);
const trendData = @json($trend->pluck('total'));
new Chart(document.getElementById('trendChart'), {
type: 'line',
data: {
labels: trendLabels,
datasets: [{
data: trendData,
borderColor: '#ef4444',
backgroundColor: 'rgba(239,68,68,0.10)',
fill: true,
tension: 0.4,
pointRadius: 5
}]
},
options: {
responsive:true,
maintainAspectRatio:false,
plugins:{
legend:{display:false}
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
drawBorder: false
}
},
x: {
grid: {
display: false,
drawBorder: false
}
}
scales:{
y:{beginAtZero:true},
x:{grid:{display:false}}
}
}
});
/* =========================
PIE CHART
========================= */
const distLabels = @json(
$distribution->pluck('category')->map(
fn($c) => ucfirst(str_replace('_',' ',$c))
)
);
const distData = @json($distribution->pluck('total'));
/*
Warna berdasarkan label:
matang = merah
mentah = hijau
setengah matang = kuning
*/
const dynamicColors = distLabels.map(label => {
if (label.toLowerCase() === 'matang') {
return '#ef4444'; // merah
}
else if (label.toLowerCase() === 'mentah') {
return '#22c55e'; // hijau
}
else {
return '#f59e0b'; // kuning/orange
}
});
new Chart(document.getElementById('distributionChart'), {
type: 'pie',
data: {
labels: distLabels,
datasets: [{
data: distData,
backgroundColor: dynamicColors,
borderWidth: 1
}]
},
options:{
responsive:true,
maintainAspectRatio:false,
plugins:{
legend:{
position:'bottom'
}
}
});
// Distribution Chart
const distributionCtx = document.getElementById('distributionChart').getContext('2d');
new Chart(distributionCtx, {
type: 'doughnut',
data: {
labels: ['Mentah', 'Setengah Matang', 'Matang'],
datasets: [{
data: [35, 40, 25],
backgroundColor: [
'rgba(34, 197, 94, 0.8)',
'rgba(249, 115, 22, 0.8)',
'rgba(236, 72, 153, 0.8)'
],
borderColor: [
'rgba(34, 197, 94, 1)',
'rgba(249, 115, 22, 1)',
'rgba(236, 72, 153, 1)'
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
},
cutout: '70%'
}
});
}
});
</script>
@endsection
@endsection

View File

@ -280,13 +280,10 @@
</button>
<div class="flex items-center space-x-3">
<div class="text-right hidden md:block">
<p class="text-sm font-medium text-gray-900">{{ Auth::user()->name ?? 'Admin User' }}</p>
<p class="text-xs text-gray-500">Administrator</p>
</div>
<div class="w-10 h-10 bg-gradient-to-br from-red-400 to-pink-400 rounded-full flex items-center justify-center">
<i class="fas fa-user text-white text-sm"></i>
</div>
<a href="{{ route('admin.logout') }}" class="sidebar-item flex items-center space-x-3 px-4 py-3 rounded-lg text-red-600 hover:bg-red-50 transition-all">
<i class="fas fa-sign-out-alt w-5"></i>
<span>Logout</span>
</a>
</div>
</div>
</div>

View File

@ -215,12 +215,11 @@ function openModal(adminId = null) {
.then(data => {
document.getElementById('name').value = data.name;
document.getElementById('email').value = data.email;
document.getElementById('role').value = data.role;
document.getElementById('password').removeAttribute('required');
})
.catch(error => {
console.error('Error:', error);
alert('Gagal memuat data admin');
alert('Gagal memuat data aadmin');
});
} else {
title.textContent = 'Tambah Admin';

View File

@ -50,15 +50,6 @@ class="sidebar-link flex items-center space-x-3 px-4 py-3 rounded-lg text-gray-7
</div>
</div>
<!-- Logout Section -->
<div class="p-4 border-t border-gray-200">
<a href="{{ route('admin.logout') }}"
class="sidebar-item flex items-center space-x-3 px-4 py-3 rounded-lg text-red-600 hover:bg-red-50 transition-all">
<i class="fas fa-sign-out-alt w-5"></i>
<span>Logout</span>
</a>
</div>
<!-- Footer -->
<div class="p-4 text-center text-xs text-gray-500 border-t border-gray-200">
Made with <span class="text-red-500">❤️</span> by TomatoScan Team

View File

@ -1,261 +1,316 @@
@extends('Admin.layouts.app')
@section('title', 'Statistik Sistem')
@section('page-title', 'Statistik Sistem')
@section('content')
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Statistik Sistem</h1>
<p class="text-gray-600">Pantau performa dan statistik sistem klasifikasi tomat</p>
<p class="text-gray-600">
Pantau performa dan statistik sistem klasifikasi tomat
</p>
</div>
<!-- Summary Cards -->
<!-- =======================================================
CARD SUMMARY
======================================================= -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Total Klasifikasi Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<!-- TOTAL -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
<i class="fas fa-chart-bar text-blue-600 text-xl"></i>
</div>
<span class="text-xs text-green-600 font-medium bg-green-50 px-2 py-1 rounded-full">
+12.5%
<span class="text-xs font-medium px-2 py-1 rounded-full
{{ $pctTotal >= 0 ? 'text-green-600 bg-green-50' : 'text-red-600 bg-red-50' }}">
{{ $pctTotal >= 0 ? '+' : '' }}{{ $pctTotal }}%
</span>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-1">8,456</h3>
<p class="text-sm text-gray-600">Total Klasifikasi</p>
<h3 class="text-3xl font-bold text-gray-900">
{{ number_format($totalKlasifikasi) }}
</h3>
<p class="text-sm text-gray-600 mt-2">
Total Klasifikasi
</p>
</div>
<!-- Klasifikasi Hari Ini Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<!-- HARI INI -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
<i class="fas fa-calendar-day text-green-600 text-xl"></i>
</div>
<span class="text-xs text-green-600 font-medium bg-green-50 px-2 py-1 rounded-full">
+8.2%
<span class="text-xs font-medium px-2 py-1 rounded-full
{{ $pctHariIni >= 0 ? 'text-green-600 bg-green-50' : 'text-red-600 bg-red-50' }}">
{{ $pctHariIni >= 0 ? '+' : '' }}{{ $pctHariIni }}%
</span>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-1">142</h3>
<p class="text-sm text-gray-600">Klasifikasi Hari Ini</p>
<h3 class="text-3xl font-bold text-gray-900">
{{ number_format($klasifikasiHariIni) }}
</h3>
<p class="text-sm text-gray-600 mt-2">
Klasifikasi Hari Ini
</p>
</div>
<!-- Akurasi Rata-rata Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<!-- AKURASI -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
<i class="fas fa-bullseye text-purple-600 text-xl"></i>
</div>
<span class="text-xs text-green-600 font-medium bg-green-50 px-2 py-1 rounded-full">
+2.1%
<span class="text-xs text-gray-600 font-medium bg-gray-50 px-2 py-1 rounded-full">
MODEL
</span>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-1">94.8%</h3>
<p class="text-sm text-gray-600">Akurasi Rata-rata</p>
<h3 class="text-3xl font-bold text-gray-900">
{{ number_format($akurasiRataRata, 2) }}%
</h3>
<p class="text-sm text-gray-600 mt-2">
Akurasi Model
</p>
</div>
<!-- Pengguna Aktif Admin Card -->
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<!-- ADMIN -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center">
<i class="fas fa-users text-orange-600 text-xl"></i>
</div>
<span class="text-xs text-gray-600 font-medium bg-gray-50 px-2 py-1 rounded-full">
0%
ADMIN
</span>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-1">12</h3>
<p class="text-sm text-gray-600">Pengguna Aktif Admin</p>
<h3 class="text-3xl font-bold text-gray-900">
{{ $penggunaAktifAdmin }}
</h3>
<p class="text-sm text-gray-600 mt-2">
Pengguna Aktif Admin
</p>
</div>
</div>
<!-- Charts Section -->
<!-- =======================================================
CHARTS
======================================================= -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Bar Chart - Jumlah Klasifikasi Harian -->
<div class="chart-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">Jumlah Klasifikasi Harian</h2>
<p class="text-sm text-gray-600">Statistik klasifikasi per hari dalam 7 hari terakhir</p>
</div>
<div class="chart-container">
<!-- BAR -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">
Jumlah Klasifikasi Harian
</h2>
<p class="text-sm text-gray-600 mb-6">
Statistik klasifikasi 7 hari terakhir
</p>
<div style="height:320px">
<canvas id="barChart"></canvas>
</div>
</div>
<!-- Line Chart - Tren Klasifikasi Bulanan -->
<div class="chart-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">Tren Klasifikasi Bulanan</h2>
<p class="text-sm text-gray-600">Perkembangan klasifikasi per bulan</p>
</div>
<div class="chart-container">
<!-- LINE -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">
Tren Klasifikasi Bulanan
</h2>
<p class="text-sm text-gray-600 mb-6">
Statistik klasifikasi 12 bulan terakhir
</p>
<div style="height:320px">
<canvas id="lineChart"></canvas>
</div>
</div>
</div>
<!-- Donut Chart Section -->
<div class="chart-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">Distribusi Kematangan Tomat</h2>
<p class="text-sm text-gray-600">Persentase hasil klasifikasi berdasarkan kategori kematangan</p>
</div>
<!-- =======================================================
PIE CHART DATASET
======================================================= -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-2">
Distribusi Dataset Training Tomat
</h2>
<p class="text-sm text-gray-600 mb-6">
Berdasarkan dataset asli model klasifikasi
</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="chart-container">
<canvas id="donutChart"></canvas>
<!-- PIE -->
<div style="height:340px">
<canvas id="pieChart"></canvas>
</div>
<!-- LEGEND -->
<div class="flex items-center justify-center">
<div class="space-y-4">
<div class="space-y-5 w-full max-w-sm">
@foreach($distribusiData as $item)
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-4 h-4 bg-green-500 rounded-full mr-3"></div>
<span class="text-sm text-gray-700">Mentah</span>
<div class="flex items-center gap-3">
<div class="w-4 h-4 rounded-full
{{ $loop->index == 0 ? 'bg-green-500' : ($loop->index == 1 ? 'bg-yellow-500' : 'bg-red-500') }}">
</div>
<span class="text-sm text-gray-700">
{{ $item['label'] }}
</span>
</div>
<span class="text-sm font-medium text-gray-900">35%</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-4 h-4 bg-yellow-500 rounded-full mr-3"></div>
<span class="text-sm text-gray-700">Setengah Matang</span>
<div class="text-right">
<div class="text-sm font-semibold text-gray-900">
{{ $item['jumlah'] }} gambar
</div>
<div class="text-xs text-gray-500">
{{ $item['persentase'] }}%
</div>
</div>
<span class="text-sm font-medium text-gray-900">40%</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-4 h-4 bg-pink-500 rounded-full mr-3"></div>
<span class="text-sm text-gray-700">Matang</span>
</div>
<span class="text-sm font-medium text-gray-900">25%</span>
</div>
@endforeach
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
// Bar Chart - Jumlah Klasifikasi Harian
const barCtx = document.getElementById('barChart').getContext('2d');
new Chart(barCtx, {
type: 'bar',
data: {
labels: ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'],
datasets: [{
label: 'Jumlah Klasifikasi',
data: [145, 189, 167, 198, 234, 178, 156],
backgroundColor: 'rgba(239, 68, 68, 0.8)',
borderColor: 'rgba(239, 68, 68, 1)',
borderWidth: 2,
borderRadius: 8,
barThickness: 40
}]
/* ==================================================
BAR CHART
================================================== */
new Chart(document.getElementById('barChart'), {
type: 'bar',
data: {
labels: @json($hariLabels),
datasets: [{
label: 'Jumlah',
data: @json($hariData),
backgroundColor: '#ef4444',
borderRadius: 8,
barThickness: 34
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display:false }
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
scales: {
y: {
beginAtZero:true,
ticks:{ precision:0 }
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
drawBorder: false
}
},
x: {
grid: {
display: false,
drawBorder: false
}
}
x: {
grid:{ display:false }
}
}
});
// Line Chart - Tren Klasifikasi Bulanan
const lineCtx = document.getElementById('lineChart').getContext('2d');
new Chart(lineCtx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
datasets: [{
label: 'Jumlah Klasifikasi',
data: [1200, 1350, 1100, 1450, 1600, 1750, 1900, 1850, 2000, 2100, 1950, 2200],
borderColor: 'rgba(239, 68, 68, 1)',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointBackgroundColor: 'rgba(239, 68, 68, 1)',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 6,
pointHoverRadius: 8
}]
}
});
/* ==================================================
LINE CHART
================================================== */
new Chart(document.getElementById('lineChart'), {
type: 'line',
data: {
labels: @json($bulanLabels),
datasets: [{
label:'Jumlah',
data: @json($bulanData),
borderColor: '#ef4444',
backgroundColor: 'rgba(239,68,68,0.10)',
fill: true,
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive:true,
maintainAspectRatio:false,
plugins:{
legend:{ display:false }
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
scales:{
y:{
beginAtZero:true,
ticks:{ precision:0 }
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
drawBorder: false
}
},
x: {
grid: {
display: false,
drawBorder: false
}
}
}
x:{ grid:{ display:false } }
}
});
// Donut Chart - Distribusi Kematangan Tomat
const donutCtx = document.getElementById('donutChart').getContext('2d');
new Chart(donutCtx, {
type: 'doughnut',
data: {
labels: ['Mentah', 'Setengah Matang', 'Matang'],
datasets: [{
data: [35, 40, 25],
backgroundColor: [
'rgba(34, 197, 94, 0.8)',
'rgba(250, 204, 21, 0.8)',
'rgba(236, 72, 153, 0.8)'
],
borderColor: [
'rgba(34, 197, 94, 1)',
'rgba(250, 204, 21, 1)',
'rgba(236, 72, 153, 1)'
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
cutout: '70%'
}
});
/* ==================================================
PIE CHART
================================================== */
new Chart(document.getElementById('pieChart'), {
type: 'pie',
data: {
labels: @json(array_column($distribusiData, 'label')),
datasets: [{
data: @json(array_column($distribusiData, 'jumlah')),
backgroundColor: [
'#22c55e',
'#eab308',
'#ef4444'
],
borderWidth: 0
}]
},
options: {
responsive:true,
maintainAspectRatio:false,
plugins:{
legend:{ display:false }
}
});
}
});
</script>
@endsection
@endsection

View File

@ -51,7 +51,7 @@
@elseif($predictionClass == 'mentah')
<div class="text-6xl mb-4">🟢</div>
<span class="px-6 py-3 rounded-full text-white font-bold text-lg bg-yellow-500">Mentah</span>
@else
@elseif($predictionClass == 'setengah_matang')
<div class="text-6xl mb-4">🟡</div>
<span class="px-6 py-3 rounded-full text-white font-bold text-lg bg-purple-500">Setengah Matang</span>
@endif
@ -68,25 +68,7 @@
</div>
</div>
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-700 mb-4">Probabilitas Setiap Kelas</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
@foreach($probabilities as $class => $prob)
<div class="bg-gray-50 rounded-lg p-4 text-center">
<h4 class="font-semibold mb-2
@if($class == 'matang') text-green-600
@elseif($class == 'mentah') text-yellow-600
@else text-purple-600 @endif">
{{ ucfirst(str_replace('_', ' ', $class)) }}
</h4>
<div class="text-2xl font-bold mb-1">{{ number_format($prob['percentage'], 1) }}%</div>
<div class="text-sm text-gray-500">{{ number_format($prob['probability'], 4) }}</div>
</div>
@endforeach
</div>
</div>
<div class="bg-gray-50 rounded-xl p-6 mb-8 text-left">
<div class="bg-gray-50 rounded-xl p-6 mb-8 text-left text-justify">
<h3 class="text-lg font-semibold text-gray-700 mb-4">Informasi Proses</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div><strong>Model:</strong> {{ $metadata['model_type'] ?? 'RandomForest' }}</div>
@ -101,10 +83,6 @@
class="inline-flex items-center bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-6 rounded-lg shadow-lg transition-all">
<i class="fas fa-redo mr-2"></i> Upload Gambar Baru
</a>
<a href="{{ route('tomat.clear') }}"
class="inline-flex items-center bg-gray-500 hover:bg-gray-600 text-white font-semibold py-3 px-6 rounded-lg shadow-lg transition-all">
<i class="fas fa-trash mr-2"></i> Clear Result
</a>
</div>
</div>
</div>

View File

@ -3,8 +3,10 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UploadController;
use App\Http\Controllers\AdminController;
use App\Http\Controllers\TomatoController;
use App\Http\Controllers\TomatController;
use App\Http\Controllers\AdminDashboardController;
use App\Http\Controllers\ClassificationHistoryController;
use App\Http\Controllers\StatistikController;
Route::get('/', function () {
return view('welcome');
@ -23,7 +25,6 @@
Route::get('/result', [TomatController::class, 'getResult'])->name('result');
Route::get('/service-status', [TomatController::class, 'checkService'])->name('service-status');
Route::get('/model-info', [TomatController::class, 'getModelInfo'])->name('model-info');
Route::get('/clear', [TomatoController::class, 'clear'])->name('clear');
});
// Legacy upload routes - redirect to new tomat routes
@ -31,10 +32,8 @@
return redirect()->route('tomat.upload');
})->name('upload.index');
Route::post('/upload', [TomatoController::class, 'upload'])->name('upload.store');
Route::get('/upload/result', function() {
return redirect()->route('tomat.result');
})->name('upload.result');
Route::post('/upload', [UploadController::class, 'store'])->name('upload.store');
Route::get('/upload/result/{id}', [UploadController::class, 'result'])->name('upload.result');
// Login route (redirect to admin login)
Route::get('/login', function () {
@ -49,13 +48,8 @@
Route::post('/admin/login', [UploadController::class, 'adminLogin'])->name('admin.login.submit');
// Admin dashboard route
Route::get('/admin/dashboard', function () {
if (!session('admin_logged_in')) {
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
}
return view('Admin.index');
})->name('admin.dashboard');
// ✅ Perbaikan
Route::get('/admin/dashboard', [AdminDashboardController::class, 'index'])->name('admin.dashboard');
// Manage admin routes
Route::get('/admin/manage-admin', [AdminController::class, 'index'])->name('admin.manage-admin');
Route::post('/admin/manage-admin', [AdminController::class, 'store'])->name('admin.manage-admin.store');
@ -65,20 +59,12 @@
Route::patch('/admin/manage-admin/{id}/toggle-status', [AdminController::class, 'toggleStatus'])->name('admin.manage-admin.toggle-status');
// Classification history route
Route::get('/admin/classification-history', function () {
if (!session('admin_logged_in')) {
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
}
return view('Admin.classification-history');
})->name('admin.classification-history');
Route::get('/admin/classification-history', [ClassificationHistoryController::class, 'index'])
->name('admin.classification-history');
// System statistics route
Route::get('/admin/system-statistics', function () {
if (!session('admin_logged_in')) {
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
}
return view('Admin.system-statistics');
})->name('admin.system-statistics');
Route::get('/admin/system-statistics', [StatistikController::class, 'index'])
->name('admin.system-statistics'); // ← pastikan ada ->name() ini!
Route::get('/admin/logout', function () {
// Clear admin session
@ -92,4 +78,4 @@
return view('upload');
});
Route::post('/upload', [TomatoController::class, 'upload'])->name('upload');
Route::post('/upload', [TomatController::class, 'upload'])->name('upload');