Perbaikan dashboard admin dan fitur statistik sistem
This commit is contained in:
parent
a4d75136f7
commit
5697938fa4
|
|
@ -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);
|
||||
$admin = User::where('id', $id)
|
||||
->where('role', 'admin')
|
||||
->firstOrFail();
|
||||
|
||||
// Pastikan hanya admin yang dapat diupdate
|
||||
if ($admin->role !== 'admin') {
|
||||
return response()->json(['error' => 'Hanya admin yang dapat dikelola'], 422);
|
||||
}
|
||||
|
||||
// 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::findOrFail($id);
|
||||
$admin = User::where('id', $id)
|
||||
->where('role', 'admin')
|
||||
->firstOrFail();
|
||||
|
||||
// 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::findOrFail($id);
|
||||
$admin = User::where('id', $id)
|
||||
->where('role', 'admin')
|
||||
->firstOrFail();
|
||||
|
||||
// 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
// Simulate AI classification (replace with actual AI logic)
|
||||
$classification = $this->classifyTomato($path);
|
||||
// Buat folder jika belum ada
|
||||
if (!file_exists(storage_path('app/public/' . $uploadPath))) {
|
||||
mkdir(storage_path('app/public/' . $uploadPath), 0755, true);
|
||||
}
|
||||
|
||||
// Redirect to result page with classification data
|
||||
return redirect()->route('upload.result', [
|
||||
'image' => $path,
|
||||
'category' => $classification['category'],
|
||||
'probability' => $classification['probability']
|
||||
// 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;
|
||||
|
||||
$apiResponse = $this->sendToFlaskAPI($file);
|
||||
|
||||
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);
|
||||
$upload = Upload::find($uploadId);
|
||||
|
||||
// Get description based on category
|
||||
$description = $this->getCategoryDescription($category, $probability);
|
||||
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') {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => 'Asia/Jakarta',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -1,187 +1,118 @@
|
|||
@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">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<img src="https://picsum.photos/seed/tomato1/50/50.jpg"
|
||||
alt="Tomato"
|
||||
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
|
||||
@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>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
23 Nov 2024, 14: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-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">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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="table-row" data-classification="setengah-matang">
|
||||
<!-- Gambar -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<img src="https://picsum.photos/seed/tomato2/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>
|
||||
<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>
|
||||
<!-- Tanggal -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
23 Nov 2024, 12:20
|
||||
{{ $upload->created_at->format('d M Y, H:i') }}
|
||||
</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">
|
||||
<!-- Klasifikasi -->
|
||||
<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
|
||||
@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>
|
||||
<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>
|
||||
<!-- Confidence -->
|
||||
<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 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>
|
||||
@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>
|
||||
|
|
@ -190,37 +121,49 @@ class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
|
|||
<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>
|
||||
<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
|
||||
|
||||
<button class="pagination-btn active px-3 py-2 text-sm font-medium border border-gray-300 rounded-lg">
|
||||
1
|
||||
</button>
|
||||
{{-- 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
|
||||
|
||||
<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>
|
||||
{{-- 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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}]
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
}]
|
||||
/* ==================================================
|
||||
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 }
|
||||
},
|
||||
cutout: '70%'
|
||||
x:{ grid:{ display:false } }
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* ==================================================
|
||||
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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue