Perbaikan dashboard admin dan fitur statistik sistem
This commit is contained in:
parent
a4d75136f7
commit
5697938fa4
|
|
@ -9,28 +9,32 @@
|
||||||
|
|
||||||
class AdminController extends Controller
|
class AdminController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
private function checkAuth()
|
||||||
{
|
{
|
||||||
if (!session('admin_logged_in')) {
|
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'
|
public function index()
|
||||||
$admins = User::where('role', 'admin')->orderBy('created_at', 'desc')->get();
|
{
|
||||||
|
$this->checkAuth();
|
||||||
|
|
||||||
|
// Ambil hanya admin
|
||||||
|
$admins = User::where('role', 'admin')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
return view('Admin.manage-admin', compact('admins'));
|
return view('Admin.manage-admin', compact('admins'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
if (!session('admin_logged_in')) {
|
$this->checkAuth();
|
||||||
return response()->json(['error' => 'Unauthorized'], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate input - role tidak termasuk, otomatis 'admin'
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'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',
|
'password' => 'required|string|min:8',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -38,110 +42,109 @@ public function store(Request $request)
|
||||||
return response()->json(['errors' => $validator->errors()], 422);
|
return response()->json(['errors' => $validator->errors()], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create admin dengan role selalu 'admin', tidak dari input
|
|
||||||
$admin = User::create([
|
$admin = User::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
'password' => Hash::make($request->password),
|
'password' => Hash::make($request->password),
|
||||||
'role' => 'admin', // Role SELALU 'admin'
|
'role' => 'admin',
|
||||||
'email_verified_at' => now()
|
'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)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
if (!session('admin_logged_in')) {
|
$this->checkAuth();
|
||||||
return response()->json(['error' => 'Unauthorized'], 401);
|
|
||||||
}
|
$admin = User::where('id', $id)
|
||||||
|
->where('role', 'admin')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
$admin = User::findOrFail($id);
|
|
||||||
return response()->json($admin);
|
return response()->json($admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
if (!session('admin_logged_in')) {
|
$this->checkAuth();
|
||||||
return response()->json(['error' => 'Unauthorized'], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'name' => 'required|string|max:255',
|
'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()) {
|
if ($validator->fails()) {
|
||||||
return response()->json(['errors' => $validator->errors()], 422);
|
return response()->json(['errors' => $validator->errors()], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update hanya name dan email, role TIDAK boleh diubah
|
|
||||||
$admin->update([
|
$admin->update([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
// Role selalu 'admin', tidak dapat diubah
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->filled('password')) {
|
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)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
if (!session('admin_logged_in')) {
|
$this->checkAuth();
|
||||||
return response()->json(['error' => 'Unauthorized'], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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')) {
|
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();
|
$admin->delete();
|
||||||
|
|
||||||
return response()->json(['success' => 'Admin berhasil dihapus']);
|
return response()->json([
|
||||||
|
'success' => 'Admin berhasil dihapus'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleStatus($id)
|
public function toggleStatus($id)
|
||||||
{
|
{
|
||||||
if (!session('admin_logged_in')) {
|
$this->checkAuth();
|
||||||
return response()->json(['error' => 'Unauthorized'], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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')) {
|
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
|
$status = $admin->email_verified_at ? null : now();
|
||||||
if ($admin->email_verified_at) {
|
|
||||||
$admin->update(['email_verified_at' => null]);
|
|
||||||
$status = 'inactive';
|
|
||||||
} else {
|
|
||||||
$admin->update(['email_verified_at' => now()]);
|
|
||||||
$status = 'active';
|
|
||||||
}
|
|
||||||
|
|
||||||
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\Http\Request;
|
||||||
use Illuminate\Support\Facades\Http;
|
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
|
class TomatController extends Controller
|
||||||
{
|
{
|
||||||
protected $apiUrl = 'http://127.0.0.1:5000/predict';
|
protected $apiUrl = 'http://127.0.0.1:5000/predict';
|
||||||
|
|
||||||
/**
|
|
||||||
* Show upload form
|
|
||||||
*/
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
return view('tomat.upload');
|
return view('tomat.upload');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle image upload dan kirim ke Flask API
|
|
||||||
*/
|
|
||||||
public function classify(Request $request)
|
public function classify(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
|
|
@ -29,8 +27,7 @@ public function classify(Request $request)
|
||||||
try {
|
try {
|
||||||
$uploadedFile = $request->file('image');
|
$uploadedFile = $request->file('image');
|
||||||
|
|
||||||
// Kirim file langsung ke Flask tanpa resize ulang di sini
|
// Kirim ke Flask (file original)
|
||||||
// Flask sudah handle resize 256x256 sendiri
|
|
||||||
$response = Http::timeout(30)
|
$response = Http::timeout(30)
|
||||||
->attach(
|
->attach(
|
||||||
'image',
|
'image',
|
||||||
|
|
@ -50,22 +47,58 @@ public function classify(Request $request)
|
||||||
return back()->with('error', 'Error dari API: ' . $errorMsg);
|
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
|
// Simpan hasil ke session
|
||||||
session([
|
session([
|
||||||
'prediction_result' => $result,
|
'prediction_result' => $result,
|
||||||
'processed_at' => now(),
|
'processed_at' => now(),
|
||||||
'original_size' => $uploadedFile->getSize(),
|
'original_size' => $uploadedFile->getSize(),
|
||||||
|
'compressed_path' => $imagePath,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()->route('tomat.result');
|
return redirect()->route('tomat.result');
|
||||||
|
|
||||||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
} 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) {
|
} catch (\Exception $e) {
|
||||||
return back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
return back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ... sisa method tidak berubah
|
||||||
/**
|
/**
|
||||||
* Show prediction result
|
* Show prediction result
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,15 @@
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use App\Models\Upload;
|
||||||
|
use Intervention\Image\Facades\Image;
|
||||||
|
|
||||||
class UploadController extends Controller
|
class UploadController extends Controller
|
||||||
{
|
{
|
||||||
|
protected $flaskApiUrl = 'http://127.0.0.1:5000/predict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the upload page.
|
* 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
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
|
@ -54,134 +59,190 @@ public function store(Request $request)
|
||||||
try {
|
try {
|
||||||
$file = $request->file('tomato_image');
|
$file = $request->file('tomato_image');
|
||||||
|
|
||||||
// Generate unique filename
|
|
||||||
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
$uploadPath = 'uploads/tomatoes';
|
$uploadPath = 'uploads/tomatoes';
|
||||||
|
|
||||||
if (!Storage::disk('public')->exists($uploadPath)) {
|
if (!Storage::disk('public')->exists($uploadPath)) {
|
||||||
Storage::disk('public')->makeDirectory($uploadPath);
|
Storage::disk('public')->makeDirectory($uploadPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the file
|
// ✅ Ganti dengan ini (kompress sebelum simpan)
|
||||||
$path = $file->storeAs($uploadPath, $filename, 'public');
|
$savePath = storage_path('app/public/' . $uploadPath . '/' . $filename);
|
||||||
|
|
||||||
// Simulate AI classification (replace with actual AI logic)
|
// Buat folder jika belum ada
|
||||||
$classification = $this->classifyTomato($path);
|
if (!file_exists(storage_path('app/public/' . $uploadPath))) {
|
||||||
|
mkdir(storage_path('app/public/' . $uploadPath), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to result page with classification data
|
// Kompress: resize max 800px, quality 75%
|
||||||
return redirect()->route('upload.result', [
|
Image::make($file->getRealPath())
|
||||||
'image' => $path,
|
->resize(400, null, function ($constraint) {
|
||||||
'category' => $classification['category'],
|
$constraint->aspectRatio();
|
||||||
'probability' => $classification['probability']
|
$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) {
|
} catch (\Exception $e) {
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('upload.index')
|
->route('upload.index')
|
||||||
->withErrors(['upload' => 'Terjadi kesalahan saat mengunggah file: ' . $e->getMessage()])
|
->withErrors(['error' => 'Terjadi kesalahan saat mengunggah file: ' . $e->getMessage()])
|
||||||
->withInput();
|
->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
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function result(Request $request)
|
public function result(Request $request)
|
||||||
{
|
{
|
||||||
// Get parameters from query string
|
$uploadId = $request->route('id') ?? $request->query('id');
|
||||||
$imagePath = $request->get('image');
|
|
||||||
$category = $request->get('category', 'matang');
|
|
||||||
$probability = (int) $request->get('probability', 85);
|
|
||||||
|
|
||||||
// Validate inputs
|
if (!$uploadId) {
|
||||||
if (!$imagePath) {
|
return redirect()->route('upload.index')
|
||||||
return redirect()->route('upload.index')->withErrors(['error' => 'Tidak ada gambar yang dianalisis.']);
|
->withErrors(['error' => 'ID upload tidak valid.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get color classes based on category
|
$upload = Upload::find($uploadId);
|
||||||
$colors = $this->getCategoryColors($category);
|
|
||||||
|
|
||||||
// Get description based on category
|
if (!$upload) {
|
||||||
$description = $this->getCategoryDescription($category, $probability);
|
return redirect()->route('upload.index')
|
||||||
|
->withErrors(['error' => 'Data klasifikasi tidak ditemukan.']);
|
||||||
|
}
|
||||||
|
|
||||||
return view('result', [
|
return view('result', [
|
||||||
'imagePath' => $imagePath,
|
'imagePath' => $upload->image_path,
|
||||||
'category' => $category,
|
'category' => $upload->category,
|
||||||
'probability' => $probability,
|
'confidence' => $upload->confidence,
|
||||||
'maturityColor' => $colors['badge'],
|
'processedAt' => $upload->created_at
|
||||||
'progressColor' => $colors['progress'],
|
|
||||||
'description' => $description
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Handle admin login.
|
||||||
*
|
*
|
||||||
|
|
@ -215,9 +276,8 @@ public function adminLogin(Request $request)
|
||||||
|
|
||||||
// Query hanya admin dengan role 'admin'
|
// Query hanya admin dengan role 'admin'
|
||||||
$user = \DB::table('users')
|
$user = \DB::table('users')
|
||||||
->where('email', $email)
|
->where('email', $email)
|
||||||
->where('role', 'admin') // Hanya admin yang dapat login
|
->first();
|
||||||
->first();
|
|
||||||
|
|
||||||
// Verifikasi password dan pastikan role adalah 'admin'
|
// Verifikasi password dan pastikan role adalah 'admin'
|
||||||
if ($user && \Hash::check($password, $user->password) && $user->role === '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",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"intervention/image": "2.7",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1"
|
||||||
"intervention/image": "^2.7"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c514d8f7b9fc5970bdd94287905ef584",
|
"content-hash": "1c02b53f5f6daedbd6f7bb6a63f7c353",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
|
@ -1052,6 +1052,90 @@
|
||||||
],
|
],
|
||||||
"time": "2025-08-22T14:27:06+00:00"
|
"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",
|
"name": "laravel/framework",
|
||||||
"version": "v12.39.0",
|
"version": "v12.39.0",
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'Asia/Jakarta',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
// Hapus semua user dengan role 'user'
|
// 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
|
// Set default role admin untuk semua user yang tersisa
|
||||||
DB::table('users')->update(['role' => 'admin']);
|
DB::table('users')->update(['role' => 'admin']);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ public function up(): void
|
||||||
Schema::create('uploads', function (Blueprint $table) {
|
Schema::create('uploads', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('image_path'); // hasil upload user
|
$table->string('image_path'); // hasil upload user
|
||||||
|
$table->string('category'); // hasil: matang / mentah / setengah_matang
|
||||||
|
$table->float('confidence', 10, 2); // nilai confidence (%)
|
||||||
$table->timestamps();
|
$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')
|
@extends('Admin.layouts.app')
|
||||||
|
|
||||||
@section('title', 'Riwayat Klasifikasi')
|
@section('title', 'Riwayat Klasifikasi')
|
||||||
|
|
||||||
@section('page-title', 'Riwayat Klasifikasi')
|
@section('page-title', 'Riwayat Klasifikasi')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Riwayat Klasifikasi</h1>
|
<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>
|
<p class="text-gray-600">Lihat semua riwayat klasifikasi tomat yang telah dilakukan</p>
|
||||||
</div>
|
</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">
|
<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="relative flex-1 max-w-md">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<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>
|
<i class="fas fa-search text-gray-400"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text"
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
id="searchInput"
|
|
||||||
placeholder="Cari riwayat…"
|
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">
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Buttons -->
|
|
||||||
<div class="flex flex-wrap gap-2">
|
<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
|
Semua
|
||||||
</button>
|
</a>
|
||||||
<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 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
|
Mentah
|
||||||
</button>
|
</a>
|
||||||
<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 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
|
Setengah Matang
|
||||||
</button>
|
</a>
|
||||||
<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 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
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Classification Table -->
|
<!-- Table -->
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead class="bg-gray-50 border-b border-gray-200">
|
<thead class="bg-gray-50 border-b border-gray-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">No</th>
|
||||||
Gambar
|
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Gambar</th>
|
||||||
</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">
|
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Klasifikasi</th>
|
||||||
Tanggal Unggah
|
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Skor Keyakinan</th>
|
||||||
</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
<!-- Sample Data Rows -->
|
@forelse ($uploads as $index => $upload)
|
||||||
<tr class="table-row" data-classification="matang">
|
<tr class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<!-- No Urut -->
|
||||||
<img src="https://picsum.photos/seed/tomato1/50/50.jpg"
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
alt="Tomato"
|
{{ ($uploads->currentPage() - 1) * $uploads->perPage() + $index + 1 }}
|
||||||
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
|
|
||||||
</td>
|
</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">
|
<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"
|
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>
|
||||||
<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">
|
<!-- Tanggal -->
|
||||||
<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">
|
<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>
|
||||||
<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">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<img src="https://picsum.photos/seed/tomato4/50/50.jpg"
|
@php
|
||||||
alt="Tomato"
|
$badgeClass = match($upload->category) {
|
||||||
class="w-12 h-12 rounded-full object-cover border-2 border-gray-200">
|
'matang' => 'bg-pink-100 text-pink-800',
|
||||||
</td>
|
'mentah' => 'bg-green-100 text-green-800',
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
'setengah_matang' => 'bg-yellow-100 text-yellow-800',
|
||||||
23 Nov 2024, 11:15
|
default => 'bg-gray-100 text-gray-800'
|
||||||
</td>
|
};
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
@endphp
|
||||||
<span class="classification-badge px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-pink-100 text-pink-800">
|
<span class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full {{ $badgeClass }}">
|
||||||
Matang
|
{{ ucfirst(str_replace('_', ' ', $upload->category)) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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">
|
<!-- Confidence -->
|
||||||
<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">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
23 Nov 2024, 10:30
|
<div class="flex items-center gap-2">
|
||||||
</td>
|
<span class="w-14">{{ $upload->confidence }}%</span>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<div class="w-24 bg-gray-200 rounded-full h-2">
|
||||||
<span class="classification-badge px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
<div class="h-2 rounded-full {{ $upload->confidence >= 80 ? 'bg-green-500' : ($upload->confidence >= 60 ? 'bg-yellow-500' : 'bg-red-400') }}"
|
||||||
Setengah Matang
|
style="width: {{ min($upload->confidence, 100) }}%"></div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-sm text-gray-700">
|
<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>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-1">
|
||||||
<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">
|
{{-- Prev --}}
|
||||||
<i class="fas fa-chevron-left"></i>
|
@if ($uploads->onFirstPage())
|
||||||
</button>
|
<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">
|
{{-- Page Numbers --}}
|
||||||
1
|
@foreach ($uploads->getUrlRange(1, $uploads->lastPage()) as $page => $url)
|
||||||
</button>
|
@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">
|
{{-- Next --}}
|
||||||
2
|
@if ($uploads->hasMorePages())
|
||||||
</button>
|
<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">
|
||||||
<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>
|
||||||
3
|
</a>
|
||||||
</button>
|
@else
|
||||||
|
<span class="px-3 py-2 text-sm text-gray-400 bg-white border border-gray-300 rounded-lg cursor-not-allowed">
|
||||||
<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>
|
||||||
4
|
</span>
|
||||||
</button>
|
@endif
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,181 +1,193 @@
|
||||||
@extends('Admin.layouts.app')
|
@extends('Admin.layouts.app')
|
||||||
|
|
||||||
@section('title', 'Dashboard')
|
@section('title', 'Dashboard')
|
||||||
|
|
||||||
@section('page-title', 'Dashboard')
|
@section('page-title', 'Dashboard')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Dashboard</h1>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Summary Cards -->
|
<!-- CARD -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<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="flex items-center justify-between mb-4">
|
<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">
|
<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>
|
<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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Akurasi Sistem Card -->
|
<!-- Akurasi -->
|
||||||
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<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 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>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Admin Aktif Card -->
|
<!-- Admin -->
|
||||||
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<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 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>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Data Hari Ini Card -->
|
<!-- Hari Ini -->
|
||||||
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<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 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>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts Section -->
|
|
||||||
|
<!-- CHART -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<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">
|
<!-- Trend -->
|
||||||
<div class="mb-6">
|
<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>
|
<h2 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
<p class="text-sm text-gray-600">Jumlah klasifikasi per hari dalam seminggu terakhir</p>
|
Trend Klasifikasi
|
||||||
</div>
|
</h2>
|
||||||
<div class="chart-container">
|
|
||||||
|
<p class="text-sm text-gray-600 mb-6">
|
||||||
|
Jumlah klasifikasi dalam 7 hari terakhir
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="height:320px">
|
||||||
<canvas id="trendChart"></canvas>
|
<canvas id="trendChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Classification Distribution -->
|
<!-- Pie -->
|
||||||
<div class="chart-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<div class="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">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 mb-2">Distribusi Klasifikasi</h2>
|
Distribusi Klasifikasi
|
||||||
<p class="text-sm text-gray-600">Persentase kategori kematangan tomat</p>
|
</h2>
|
||||||
</div>
|
|
||||||
<div class="chart-container">
|
<p class="text-sm text-gray-600 mb-6">
|
||||||
|
Persentase kategori kematangan tomat
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="height:320px">
|
||||||
<canvas id="distributionChart"></canvas>
|
<canvas id="distributionChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
<script>
|
<script>
|
||||||
// Trend Chart
|
|
||||||
const trendCtx = document.getElementById('trendChart').getContext('2d');
|
/* =========================
|
||||||
new Chart(trendCtx, {
|
TREND LINE
|
||||||
type: 'line',
|
========================= */
|
||||||
data: {
|
const trendLabels = @json(
|
||||||
labels: ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'],
|
$trend->pluck('date')->map(fn($d) =>
|
||||||
datasets: [{
|
\Carbon\Carbon::parse($d)->translatedFormat('D')
|
||||||
label: 'Jumlah Klasifikasi',
|
)
|
||||||
data: [180, 220, 195, 240, 210, 185, 230],
|
);
|
||||||
borderColor: 'rgba(239, 68, 68, 1)',
|
|
||||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
const trendData = @json($trend->pluck('total'));
|
||||||
borderWidth: 3,
|
|
||||||
fill: true,
|
new Chart(document.getElementById('trendChart'), {
|
||||||
tension: 0.4,
|
type: 'line',
|
||||||
pointBackgroundColor: 'rgba(239, 68, 68, 1)',
|
data: {
|
||||||
pointBorderColor: '#fff',
|
labels: trendLabels,
|
||||||
pointBorderWidth: 2,
|
datasets: [{
|
||||||
pointRadius: 6,
|
data: trendData,
|
||||||
pointHoverRadius: 8
|
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: {
|
scales:{
|
||||||
responsive: true,
|
y:{beginAtZero:true},
|
||||||
maintainAspectRatio: false,
|
x:{grid:{display:false}}
|
||||||
plugins: {
|
}
|
||||||
legend: {
|
}
|
||||||
display: false
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
/* =========================
|
||||||
y: {
|
PIE CHART
|
||||||
beginAtZero: true,
|
========================= */
|
||||||
grid: {
|
const distLabels = @json(
|
||||||
color: 'rgba(0, 0, 0, 0.05)',
|
$distribution->pluck('category')->map(
|
||||||
drawBorder: false
|
fn($c) => ucfirst(str_replace('_',' ',$c))
|
||||||
}
|
)
|
||||||
},
|
);
|
||||||
x: {
|
|
||||||
grid: {
|
const distData = @json($distribution->pluck('total'));
|
||||||
display: false,
|
|
||||||
drawBorder: false
|
/*
|
||||||
}
|
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>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -280,13 +280,10 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<div class="text-right hidden md:block">
|
<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">
|
||||||
<p class="text-sm font-medium text-gray-900">{{ Auth::user()->name ?? 'Admin User' }}</p>
|
<i class="fas fa-sign-out-alt w-5"></i>
|
||||||
<p class="text-xs text-gray-500">Administrator</p>
|
<span>Logout</span>
|
||||||
</div>
|
</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -215,12 +215,11 @@ function openModal(adminId = null) {
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('name').value = data.name;
|
document.getElementById('name').value = data.name;
|
||||||
document.getElementById('email').value = data.email;
|
document.getElementById('email').value = data.email;
|
||||||
document.getElementById('role').value = data.role;
|
|
||||||
document.getElementById('password').removeAttribute('required');
|
document.getElementById('password').removeAttribute('required');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
alert('Gagal memuat data admin');
|
alert('Gagal memuat data aadmin');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
title.textContent = 'Tambah Admin';
|
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>
|
||||||
</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 -->
|
<!-- Footer -->
|
||||||
<div class="p-4 text-center text-xs text-gray-500 border-t border-gray-200">
|
<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
|
Made with <span class="text-red-500">❤️</span> by TomatoScan Team
|
||||||
|
|
|
||||||
|
|
@ -1,261 +1,316 @@
|
||||||
@extends('Admin.layouts.app')
|
@extends('Admin.layouts.app')
|
||||||
|
|
||||||
@section('title', 'Statistik Sistem')
|
@section('title', 'Statistik Sistem')
|
||||||
|
|
||||||
@section('page-title', 'Statistik Sistem')
|
@section('page-title', 'Statistik Sistem')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Statistik Sistem</h1>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Summary Cards -->
|
<!-- =======================================================
|
||||||
|
CARD SUMMARY
|
||||||
|
======================================================= -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<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="flex items-center justify-between mb-4">
|
||||||
|
|
||||||
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
|
<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>
|
<i class="fas fa-chart-bar text-blue-600 text-xl"></i>
|
||||||
</div>
|
</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>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Klasifikasi Hari Ini Card -->
|
<!-- HARI INI -->
|
||||||
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<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="flex items-center justify-between mb-4">
|
||||||
|
|
||||||
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
|
<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>
|
<i class="fas fa-calendar-day text-green-600 text-xl"></i>
|
||||||
</div>
|
</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>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Akurasi Rata-rata Card -->
|
<!-- AKURASI -->
|
||||||
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<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="flex items-center justify-between mb-4">
|
||||||
|
|
||||||
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
|
<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>
|
<i class="fas fa-bullseye text-purple-600 text-xl"></i>
|
||||||
</div>
|
</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>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Pengguna Aktif Admin Card -->
|
<!-- ADMIN -->
|
||||||
<div class="stat-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<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="flex items-center justify-between mb-4">
|
||||||
|
|
||||||
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center">
|
<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>
|
<i class="fas fa-users text-orange-600 text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="text-xs text-gray-600 font-medium bg-gray-50 px-2 py-1 rounded-full">
|
<span class="text-xs text-gray-600 font-medium bg-gray-50 px-2 py-1 rounded-full">
|
||||||
0%
|
ADMIN
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts Section -->
|
|
||||||
|
<!-- =======================================================
|
||||||
|
CHARTS
|
||||||
|
======================================================= -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
<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">
|
<!-- BAR -->
|
||||||
<div class="mb-6">
|
<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">Statistik klasifikasi per hari dalam 7 hari terakhir</p>
|
<h2 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
</div>
|
Jumlah Klasifikasi Harian
|
||||||
<div class="chart-container">
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-600 mb-6">
|
||||||
|
Statistik klasifikasi 7 hari terakhir
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="height:320px">
|
||||||
<canvas id="barChart"></canvas>
|
<canvas id="barChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Line Chart - Tren Klasifikasi Bulanan -->
|
<!-- LINE -->
|
||||||
<div class="chart-card bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<div class="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>
|
<h2 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
<p class="text-sm text-gray-600">Perkembangan klasifikasi per bulan</p>
|
Tren Klasifikasi Bulanan
|
||||||
</div>
|
</h2>
|
||||||
<div class="chart-container">
|
|
||||||
|
<p class="text-sm text-gray-600 mb-6">
|
||||||
|
Statistik klasifikasi 12 bulan terakhir
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="height:320px">
|
||||||
<canvas id="lineChart"></canvas>
|
<canvas id="lineChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
PIE CHART DATASET
|
||||||
<p class="text-sm text-gray-600">Persentase hasil klasifikasi berdasarkan kategori kematangan</p>
|
======================================================= -->
|
||||||
</div>
|
<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="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>
|
</div>
|
||||||
|
|
||||||
|
<!-- LEGEND -->
|
||||||
<div class="flex items-center justify-center">
|
<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 justify-between">
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-4 h-4 bg-green-500 rounded-full mr-3"></div>
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-sm text-gray-700">Mentah</span>
|
|
||||||
|
<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>
|
</div>
|
||||||
<span class="text-sm font-medium text-gray-900">35%</span>
|
|
||||||
</div>
|
<div class="text-right">
|
||||||
<div class="flex items-center justify-between">
|
<div class="text-sm font-semibold text-gray-900">
|
||||||
<div class="flex items-center">
|
{{ $item['jumlah'] }} gambar
|
||||||
<div class="w-4 h-4 bg-yellow-500 rounded-full mr-3"></div>
|
</div>
|
||||||
<span class="text-sm text-gray-700">Setengah Matang</span>
|
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{{ $item['persentase'] }}%
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
|
@endforeach
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
<script>
|
<script>
|
||||||
// Bar Chart - Jumlah Klasifikasi Harian
|
|
||||||
const barCtx = document.getElementById('barChart').getContext('2d');
|
/* ==================================================
|
||||||
new Chart(barCtx, {
|
BAR CHART
|
||||||
type: 'bar',
|
================================================== */
|
||||||
data: {
|
new Chart(document.getElementById('barChart'), {
|
||||||
labels: ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'],
|
type: 'bar',
|
||||||
datasets: [{
|
data: {
|
||||||
label: 'Jumlah Klasifikasi',
|
labels: @json($hariLabels),
|
||||||
data: [145, 189, 167, 198, 234, 178, 156],
|
datasets: [{
|
||||||
backgroundColor: 'rgba(239, 68, 68, 0.8)',
|
label: 'Jumlah',
|
||||||
borderColor: 'rgba(239, 68, 68, 1)',
|
data: @json($hariData),
|
||||||
borderWidth: 2,
|
backgroundColor: '#ef4444',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
barThickness: 40
|
barThickness: 34
|
||||||
}]
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display:false }
|
||||||
},
|
},
|
||||||
options: {
|
scales: {
|
||||||
responsive: true,
|
y: {
|
||||||
maintainAspectRatio: false,
|
beginAtZero:true,
|
||||||
plugins: {
|
ticks:{ precision:0 }
|
||||||
legend: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
scales: {
|
x: {
|
||||||
y: {
|
grid:{ display:false }
|
||||||
beginAtZero: true,
|
|
||||||
grid: {
|
|
||||||
color: 'rgba(0, 0, 0, 0.05)',
|
|
||||||
drawBorder: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
grid: {
|
|
||||||
display: false,
|
|
||||||
drawBorder: 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');
|
LINE CHART
|
||||||
new Chart(donutCtx, {
|
================================================== */
|
||||||
type: 'doughnut',
|
new Chart(document.getElementById('lineChart'), {
|
||||||
data: {
|
type: 'line',
|
||||||
labels: ['Mentah', 'Setengah Matang', 'Matang'],
|
data: {
|
||||||
datasets: [{
|
labels: @json($bulanLabels),
|
||||||
data: [35, 40, 25],
|
datasets: [{
|
||||||
backgroundColor: [
|
label:'Jumlah',
|
||||||
'rgba(34, 197, 94, 0.8)',
|
data: @json($bulanData),
|
||||||
'rgba(250, 204, 21, 0.8)',
|
borderColor: '#ef4444',
|
||||||
'rgba(236, 72, 153, 0.8)'
|
backgroundColor: 'rgba(239,68,68,0.10)',
|
||||||
],
|
fill: true,
|
||||||
borderColor: [
|
tension: 0.4,
|
||||||
'rgba(34, 197, 94, 1)',
|
pointRadius: 4,
|
||||||
'rgba(250, 204, 21, 1)',
|
pointHoverRadius: 6
|
||||||
'rgba(236, 72, 153, 1)'
|
}]
|
||||||
],
|
},
|
||||||
borderWidth: 2
|
options: {
|
||||||
}]
|
responsive:true,
|
||||||
|
maintainAspectRatio:false,
|
||||||
|
plugins:{
|
||||||
|
legend:{ display:false }
|
||||||
},
|
},
|
||||||
options: {
|
scales:{
|
||||||
responsive: true,
|
y:{
|
||||||
maintainAspectRatio: false,
|
beginAtZero:true,
|
||||||
plugins: {
|
ticks:{ precision:0 }
|
||||||
legend: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
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>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
@elseif($predictionClass == 'mentah')
|
@elseif($predictionClass == 'mentah')
|
||||||
<div class="text-6xl mb-4">🟢</div>
|
<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>
|
<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>
|
<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>
|
<span class="px-6 py-3 rounded-full text-white font-bold text-lg bg-purple-500">Setengah Matang</span>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -68,25 +68,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-8">
|
<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">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">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-700 mb-4">Informasi Proses</h3>
|
<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 class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
<div><strong>Model:</strong> {{ $metadata['model_type'] ?? 'RandomForest' }}</div>
|
<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">
|
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
|
<i class="fas fa-redo mr-2"></i> Upload Gambar Baru
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\UploadController;
|
use App\Http\Controllers\UploadController;
|
||||||
use App\Http\Controllers\AdminController;
|
use App\Http\Controllers\AdminController;
|
||||||
use App\Http\Controllers\TomatoController;
|
|
||||||
use App\Http\Controllers\TomatController;
|
use App\Http\Controllers\TomatController;
|
||||||
|
use App\Http\Controllers\AdminDashboardController;
|
||||||
|
use App\Http\Controllers\ClassificationHistoryController;
|
||||||
|
use App\Http\Controllers\StatistikController;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return view('welcome');
|
return view('welcome');
|
||||||
|
|
@ -23,7 +25,6 @@
|
||||||
Route::get('/result', [TomatController::class, 'getResult'])->name('result');
|
Route::get('/result', [TomatController::class, 'getResult'])->name('result');
|
||||||
Route::get('/service-status', [TomatController::class, 'checkService'])->name('service-status');
|
Route::get('/service-status', [TomatController::class, 'checkService'])->name('service-status');
|
||||||
Route::get('/model-info', [TomatController::class, 'getModelInfo'])->name('model-info');
|
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
|
// Legacy upload routes - redirect to new tomat routes
|
||||||
|
|
@ -31,10 +32,8 @@
|
||||||
return redirect()->route('tomat.upload');
|
return redirect()->route('tomat.upload');
|
||||||
})->name('upload.index');
|
})->name('upload.index');
|
||||||
|
|
||||||
Route::post('/upload', [TomatoController::class, 'upload'])->name('upload.store');
|
Route::post('/upload', [UploadController::class, 'store'])->name('upload.store');
|
||||||
Route::get('/upload/result', function() {
|
Route::get('/upload/result/{id}', [UploadController::class, 'result'])->name('upload.result');
|
||||||
return redirect()->route('tomat.result');
|
|
||||||
})->name('upload.result');
|
|
||||||
|
|
||||||
// Login route (redirect to admin login)
|
// Login route (redirect to admin login)
|
||||||
Route::get('/login', function () {
|
Route::get('/login', function () {
|
||||||
|
|
@ -49,13 +48,8 @@
|
||||||
Route::post('/admin/login', [UploadController::class, 'adminLogin'])->name('admin.login.submit');
|
Route::post('/admin/login', [UploadController::class, 'adminLogin'])->name('admin.login.submit');
|
||||||
|
|
||||||
// Admin dashboard route
|
// Admin dashboard route
|
||||||
Route::get('/admin/dashboard', function () {
|
// ✅ Perbaikan
|
||||||
if (!session('admin_logged_in')) {
|
Route::get('/admin/dashboard', [AdminDashboardController::class, 'index'])->name('admin.dashboard');
|
||||||
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
|
|
||||||
}
|
|
||||||
return view('Admin.index');
|
|
||||||
})->name('admin.dashboard');
|
|
||||||
|
|
||||||
// Manage admin routes
|
// Manage admin routes
|
||||||
Route::get('/admin/manage-admin', [AdminController::class, 'index'])->name('admin.manage-admin');
|
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');
|
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');
|
Route::patch('/admin/manage-admin/{id}/toggle-status', [AdminController::class, 'toggleStatus'])->name('admin.manage-admin.toggle-status');
|
||||||
|
|
||||||
// Classification history route
|
// Classification history route
|
||||||
Route::get('/admin/classification-history', function () {
|
Route::get('/admin/classification-history', [ClassificationHistoryController::class, 'index'])
|
||||||
if (!session('admin_logged_in')) {
|
->name('admin.classification-history');
|
||||||
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
|
|
||||||
}
|
|
||||||
return view('Admin.classification-history');
|
|
||||||
})->name('admin.classification-history');
|
|
||||||
|
|
||||||
// System statistics route
|
// System statistics route
|
||||||
Route::get('/admin/system-statistics', function () {
|
Route::get('/admin/system-statistics', [StatistikController::class, 'index'])
|
||||||
if (!session('admin_logged_in')) {
|
->name('admin.system-statistics'); // ← pastikan ada ->name() ini!
|
||||||
return redirect()->route('admin.login')->with('error', 'Silakan login terlebih dahulu.');
|
|
||||||
}
|
|
||||||
return view('Admin.system-statistics');
|
|
||||||
})->name('admin.system-statistics');
|
|
||||||
|
|
||||||
Route::get('/admin/logout', function () {
|
Route::get('/admin/logout', function () {
|
||||||
// Clear admin session
|
// Clear admin session
|
||||||
|
|
@ -92,4 +78,4 @@
|
||||||
return view('upload');
|
return view('upload');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::post('/upload', [TomatoController::class, 'upload'])->name('upload');
|
Route::post('/upload', [TomatController::class, 'upload'])->name('upload');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue