Add classification status, metrics, and UI updates

This commit is contained in:
kizadl 2026-04-28 00:49:19 +07:00
parent e639b0ac57
commit 44e69d13c5
7 changed files with 105 additions and 9 deletions

View File

@ -34,11 +34,20 @@ public function index()
// Hitung Total Scan (sudah terfilter role)
$totalScan = (clone $query)->count();
// Rata-rata akurasi dari yang berhasil
$avgAccuracy = (clone $query)->where('status', 'Berhasil')->avg('confidence') ?? 0;
// Tingkat keberhasilan (Total Berhasil / Total Scan)
$totalBerhasil = (clone $query)->where('status', 'Berhasil')->count();
$successRate = $totalScan > 0 ? ($totalBerhasil / $totalScan) * 100 : 0;
return inertia('admin/Dashboard', [
'user' => $user,
'chartData' => $chartData,
'recentHistory' => $recentHistory,
'totalScan' => $totalScan,
'avgAccuracy' => round($avgAccuracy, 2),
'successRate' => round($successRate, 2),
]);
}
}

View File

@ -44,11 +44,15 @@ public function predict(Request $request)
// 2. Jika sukses, simpan gambar dan hasil ke database
$path = $image->store('classifications', 'public');
$confidence = (float) filter_var($data['confidence'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
$status = $confidence >= 90 ? 'Berhasil' : 'Gagal';
Classification::create([
'user_id' => auth()->id() ?? null,
'image_path' => $path,
'result' => $data['label'],
'confidence' => (float) filter_var($data['confidence'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION),
'confidence' => $confidence,
'status' => $status,
]);
return response()->json([

View File

@ -11,7 +11,8 @@ class Classification extends Model
'user_id',
'image_path',
'result',
'confidence'
'confidence',
'status'
];
// Relasi ke User (Satu klasifikasi dimiliki satu User)

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('classifications', function (Blueprint $table) {
$table->string('status')->after('confidence')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('classifications', function (Blueprint $table) {
$table->dropColumn('status');
});
}
};

View File

@ -79,12 +79,33 @@
</div>
<transition enter-active-class="transition duration-500 ease-out" enter-from-class="transform scale-95 opacity-0" enter-to-class="transform scale-100 opacity-100">
<div v-if="predictionResult" class="mt-8 p-8 rounded-3xl bg-emerald-600 shadow-[0_0_50px_-12px_rgba(16,185,129,0.5)] text-center relative overflow-hidden">
<div
v-if="predictionResult"
class="mt-8 p-8 rounded-3xl shadow-[0_0_50px_-12px_rgba(16,185,129,0.5)] text-center relative overflow-hidden transition-colors duration-500"
:class="parseFloat(predictionResult.confidence.replace('%', '')) >= 90
? 'bg-emerald-600 shadow-emerald-500/50'
: 'bg-red-600 shadow-red-500/50'"
>
<div class="relative z-10">
<p class="text-xs font-black text-emerald-100 uppercase tracking-[0.3em] mb-2">Hasil Klasifikasi AI</p>
<h4 class="text-5xl font-black text-white italic mb-2 tracking-tighter">{{ predictionResult.label }}</h4>
<div class="inline-block px-4 py-1.5 bg-black/20 rounded-full backdrop-blur-md">
<span class="text-emerald-500 font-bold text-sm uppercase tracking-widest">Confidence: {{ predictionResult.confidence }}</span>
<h4 class="text-5xl font-black text-white italic mb-4 tracking-tighter">{{ predictionResult.label }}</h4>
<div class="flex flex-wrap justify-center gap-3">
<div class="px-4 py-1.5 bg-black/20 rounded-full backdrop-blur-md border border-white/10">
<span class="text-emerald-400 font-bold text-xs uppercase tracking-widest">Confidence: {{ predictionResult.confidence }}</span>
</div>
<div
class="px-4 py-1.5 rounded-full backdrop-blur-md border flex items-center gap-2"
:class="parseFloat(predictionResult.confidence.replace('%', '')) >= 90
? 'bg-emerald-500/20 border-emerald-500/50 text-emerald-200'
: 'bg-red-500/20 border-red-500/50 text-red-200'"
>
<div :class="['w-1.5 h-1.5 rounded-full', parseFloat(predictionResult.confidence.replace('%', '')) >= 90 ? 'bg-emerald-400' : 'bg-red-400']"></div>
<span class="font-bold text-xs uppercase tracking-widest">
{{ parseFloat(predictionResult.confidence.replace('%', '')) >= 90 ? 'Berhasil' : 'Gagal' }}
</span>
</div>
</div>
</div>
<Bean class="absolute -right-10 -bottom-10 w-40 h-40 text-white/10 rotate-12" />

View File

@ -27,7 +27,9 @@ const props = defineProps<{
user: User,
chartData: Array<{ result: string, total: number }>,
recentHistory: Array<any>,
totalScan: number
totalScan: number,
avgAccuracy: number,
successRate: number
}>();
// Konfigurasi Warna & Data untuk Pie Chart
@ -49,6 +51,7 @@ const chartOptions = {
}
}
};
</script>
<template>
@ -77,6 +80,26 @@ const chartOptions = {
<p class="text-xs text-muted-foreground">Akumulasi seluruh pemindaian</p>
</CardContent>
</Card>
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Rata-rata Akurasi</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold text-emerald-600">{{ avgAccuracy }}%</div>
<p class="text-xs text-muted-foreground">Dari klasifikasi berstatus berhasil</p>
</CardContent>
</Card>
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Tingkat Keberhasilan</CardTitle>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold text-blue-600">{{ successRate }}%</div>
<p class="text-xs text-muted-foreground">Persentase status berhasil dari total scan</p>
</CardContent>
</Card>
</div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-7">

View File

@ -132,6 +132,15 @@ const toggleScanner = () => {
{{ new Date(item.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'short' }) }}
</div>
<h3 class="text-xl font-black text-emerald-600 dark:text-emerald-500 italic truncate leading-none">{{ item.result }}</h3>
<div
class="mt-2 inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border"
:class="(item.status === 'Berhasil' || (!item.status && Number(item.confidence) >= 90))
? 'bg-emerald-50 text-emerald-600 border-emerald-200 dark:bg-emerald-950/30 dark:text-emerald-400 dark:border-emerald-800'
: 'bg-red-50 text-red-600 border-red-200 dark:bg-red-950/30 dark:text-red-400 dark:border-red-800'"
>
<div :class="['w-1.5 h-1.5 rounded-full mr-1.5', (item.status === 'Berhasil' || (!item.status && Number(item.confidence) >= 90)) ? 'bg-emerald-500' : 'bg-red-500']"></div>
{{ item.status || (Number(item.confidence) >= 90 ? 'Berhasil' : 'Gagal') }}
</div>
</div>
<div class="text-right">
<p class="text-[9px] font-bold text-zinc-400 uppercase tracking-widest">Confidence</p>
@ -142,7 +151,8 @@ const toggleScanner = () => {
<div class="space-y-2">
<div class="h-1.5 w-full bg-zinc-100 dark:bg-zinc-800 rounded-full overflow-hidden">
<div
class="h-full bg-emerald-500 rounded-full transition-all duration-1000"
class="h-full rounded-full transition-all duration-1000"
:class="(item.status === 'Berhasil' || (!item.status && Number(item.confidence) >= 90)) ? 'bg-emerald-500' : 'bg-red-500'"
:style="{ width: item.confidence + '%' }"
></div>
</div>