This commit is contained in:
destudenislam 2026-07-07 18:54:17 +07:00
parent 0c4ac2449d
commit 9f1df18c43
9 changed files with 253 additions and 17 deletions

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Classification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AdminController extends Controller
{
public function dashboard()
{
$totalUsers = User::where('role', 'user')->count();
$totalClassifications = Classification::count();
$statistikKelas = Classification::select('label', DB::raw('count(*) as total'))
->groupBy('label')
->get();
return view('admin.dashboard', compact('totalUsers', 'totalClassifications', 'statistikKelas'));
}
public function users()
{
$users = User::where('role', 'user')->latest()->get();
return view('admin.users', compact('users'));
}
public function destroyUser($id)
{
$user = User::findOrFail($id);
if ($user->role === 'admin') {
return back()->withErrors(['error' => 'Akun admin tidak dapat dihapus.']);
}
$user->delete();
return back()->with('success', 'Pengguna berhasil dihapus.');
}
public function exportCsv()
{
$classifications = Classification::all();
$filename = "laporan_klasifikasi_kopi.csv";
$handle = fopen('php://memory', 'w');
// Membuat Baris Header CSV
fputcsv($handle, ['ID', 'User ID', 'Hasil Klasifikasi', 'Akurasi (%)', 'Tanggal Pemindaian']);
// Mengisi Baris Data
foreach ($classifications as $row) {
fputcsv($handle, [
$row->id,
$row->user_id,
$row->label,
$row->confidence,
$row->created_at
]);
}
fseek($handle, 0);
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
];
return response()->stream(function () use ($handle) {
fpassthru($handle);
}, 200, $headers);
}
}

View File

@ -64,5 +64,6 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class, 'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'admin' => \App\Http\Middleware\AdminMiddleware::class,
]; ];
} }

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AdminMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (!auth()->check() || auth()->user()->role !== 'admin') {
abort(403, 'Akses ditolak. Halaman ini khusus untuk Admin.');
}
return $next($request);
}
}

View File

@ -18,10 +18,13 @@ class User extends Authenticatable
* @var array<int, string> * @var array<int, string>
*/ */
protected $fillable = [ protected $fillable = [
'name', 'name',
'email', 'email',
'password', 'password',
]; 'role',
];
/** /**
* The attributes that should be hidden for serialization. * The attributes that should be hidden for serialization.
@ -42,4 +45,8 @@ class User extends Authenticatable
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
]; ];
public function isAdmin(): bool
{
return $this->role === 'admin';
}
} }

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('role', ['user', 'admin'])->default('user')->after('password');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};

View File

@ -0,0 +1,40 @@
@extends('layouts.app')
@section('content')
<div class="max-w-6xl mx-auto px-10 py-16">
<h1 class="text-3xl font-extrabold text-gray-900 mb-8">Dashboard Admin</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-2xl shadow-sm border border-orange-100 p-8">
<h3 class="text-sm font-bold text-gray-500 uppercase tracking-widest mb-2">Total Pengguna Terdaftar</h3>
<p class="text-4xl font-black text-orange-700">{{ $totalUsers }}</p>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-orange-100 p-8">
<h3 class="text-sm font-bold text-gray-500 uppercase tracking-widest mb-2">Total Klasifikasi Dilakukan</h3>
<p class="text-4xl font-black text-orange-700">{{ $totalClassifications }}</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-orange-100 p-8 mb-8">
<h3 class="text-sm font-bold text-gray-500 uppercase tracking-widest mb-6">Statistik Klasifikasi per Kelas</h3>
<div class="space-y-4">
@forelse ($statistikKelas as $item)
<div class="flex items-center justify-between">
<span class="font-bold text-gray-900 capitalize">{{ $item->label }}</span>
<span class="px-4 py-1 bg-orange-50 text-orange-700 rounded-full text-sm font-bold">{{ $item->total }} kali</span>
</div>
@empty
<p class="text-gray-400">Belum ada data klasifikasi.</p>
@endforelse
</div>
</div>
<a href="{{ route('admin.users') }}" class="inline-block px-8 py-4 bg-orange-700 text-white rounded-full font-bold shadow-lg hover:bg-orange-800 transition">
Kelola Data Pengguna
</a>
<a href="{{ route('admin.export') }}" class="inline-block text-center px-8 py-4 bg-gray-800 text-white rounded-full font-bold shadow-lg hover:bg-black transition">
Export Data CSV
</a>
</div>
@endsection

View File

@ -0,0 +1,59 @@
@extends('layouts.app')
@section('content')
<div class="max-w-6xl mx-auto px-10 py-16">
<h1 class="text-3xl font-extrabold text-gray-900 mb-8">Kelola Data Pengguna</h1>
@if (session('success'))
<div class="mb-6 px-6 py-4 bg-green-50 text-green-700 rounded-2xl border border-green-200">
{{ session('success') }}
</div>
@endif
@if ($errors->any())
<div class="mb-6 px-6 py-4 bg-red-50 text-red-700 rounded-2xl border border-red-200">
{{ $errors->first() }}
</div>
@endif
<div class="bg-white rounded-2xl shadow-sm border border-orange-100 overflow-hidden">
<table class="w-full text-left">
<thead class="bg-orange-50">
<tr>
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">Nama</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">Email</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">Terdaftar</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-widest">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($users as $user)
<tr class="border-t border-gray-100">
<td class="px-6 py-4">{{ $user->name }}</td>
<td class="px-6 py-4">{{ $user->email }}</td>
<td class="px-6 py-4">{{ $user->created_at->format('d-m-Y') }}</td>
<td class="px-6 py-4">
<form action="{{ route('admin.users.destroy', $user->id) }}" method="POST"
onsubmit="return confirm('Yakin ingin menghapus pengguna ini?');">
@csrf
@method('DELETE')
<button type="submit" class="px-4 py-2 bg-red-50 text-red-700 rounded-full text-xs font-bold hover:bg-red-100 transition">
Hapus
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-8 text-center text-gray-400">Belum ada pengguna terdaftar.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<a href="{{ route('admin.dashboard') }}" class="inline-block mt-8 text-orange-700 font-bold hover:text-orange-800">
Kembali ke Dashboard
</a>
</div>
@endsection

View File

@ -32,20 +32,26 @@
</a> </a>
@endguest @endguest
@auth @auth
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<div class="flex flex-col items-end"> @if(Auth::user()->role === 'admin')
<span class="text-[10px] font-black uppercase text-gray-400 tracking-widest">Logged in as</span> <a href="{{ route('admin.dashboard') }}" class="px-5 py-2 bg-orange-700 text-white text-xs font-bold rounded-full hover:bg-orange-800 transition">
<span class="text-sm font-bold text-gray-900">{{ Auth::user()->name }}</span> Admin Panel
</div> </a>
@endif
<form action="{{ route('logout') }}" method="POST" class="inline"> <div class="flex flex-col items-end">
@csrf <span class="text-[10px] font-black uppercase text-gray-400 tracking-widest">Logged in as</span>
<button type="submit" class="px-5 py-2 bg-gray-100 text-gray-600 text-xs font-bold rounded-full hover:bg-red-50 hover:text-red-600 transition"> <span class="text-sm font-bold text-gray-900">{{ Auth::user()->name }}</span>
Keluar </div>
</button>
</form> <form action="{{ route('logout') }}" method="POST" class="inline">
</div> @csrf
@endauth <button type="submit" class="px-5 py-2 bg-gray-100 text-gray-600 text-xs font-bold rounded-full hover:bg-red-50 hover:text-red-600 transition">
Keluar
</button>
</form>
</div>
@endauth
</div> </div>
</nav> </nav>

View File

@ -5,6 +5,7 @@
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\ClassificationController; use App\Http\Controllers\ClassificationController;
use App\Http\Controllers\AdminController;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -73,5 +74,12 @@
Route::post('/logout', [AuthController::class, 'logout'])->name('logout'); Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
}); });
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminController::class, 'dashboard'])->name('dashboard');
Route::get('/users', [AdminController::class, 'users'])->name('users');
Route::delete('/users/{id}', [AdminController::class, 'destroyUser'])->name('users.destroy');
Route::get('/export', [AdminController::class, 'exportCsv'])->name('export');
});
Route::post('/api/classify', [ClassificationController::class, 'process'])->name('api.classify'); Route::post('/api/classify', [ClassificationController::class, 'process'])->name('api.classify');
Route::post('/api/save-result', [ClassificationController::class, 'store'])->name('api.save'); Route::post('/api/save-result', [ClassificationController::class, 'store'])->name('api.save');