update ROLE
This commit is contained in:
parent
45d641519c
commit
e38215b1b4
|
|
@ -5,14 +5,40 @@
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use App\Models\Classification; // Import model Classification
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// Mulai Query Dasar
|
||||||
|
$query = Classification::query();
|
||||||
|
|
||||||
|
// LOGIKA ROLE: Jika yang login adalah 'user', kunci hanya data miliknya
|
||||||
|
if ($user->role === 'user') {
|
||||||
|
$query->where('user_id', $user->id);
|
||||||
|
}
|
||||||
|
// Jika admin, biarkan query mengambil semua data (tanpa where)
|
||||||
|
|
||||||
|
// Ambil data Chart (sudah terfilter role)
|
||||||
|
$chartData = (clone $query)->select('result', DB::raw('count(*) as total'))
|
||||||
|
->groupBy('result')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Ambil History Terbaru (sudah terfilter role)
|
||||||
|
$recentHistory = (clone $query)->latest()->limit(10)->get();
|
||||||
|
|
||||||
|
// Hitung Total Scan (sudah terfilter role)
|
||||||
|
$totalScan = (clone $query)->count();
|
||||||
|
|
||||||
return inertia('admin/Dashboard', [
|
return inertia('admin/Dashboard', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
|
'chartData' => $chartData,
|
||||||
|
'recentHistory' => $recentHistory,
|
||||||
|
'totalScan' => $totalScan,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Public;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Classification;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ClassificationController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'image' => 'required|image|max:2048',
|
||||||
|
'result' => 'required|string',
|
||||||
|
'confidence' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simpan gambar ke storage public
|
||||||
|
$path = $request->file('image')->store('classifications', 'public');
|
||||||
|
|
||||||
|
// Simpan ke Database
|
||||||
|
Classification::create([
|
||||||
|
'user_id' => auth()->id(), // null jika guest, id jika login
|
||||||
|
'image_path' => $path,
|
||||||
|
'result' => $request->result,
|
||||||
|
'confidence' => (float) filter_var($request->confidence, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Data tersimpan ke history']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,12 +39,18 @@ public function share(Request $request): array
|
||||||
...parent::share($request),
|
...parent::share($request),
|
||||||
'name' => config('app.name'),
|
'name' => config('app.name'),
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $request->user(),
|
'user' => $request->user() ? [
|
||||||
|
'id' => $request->user()->id,
|
||||||
|
'name' => $request->user()->name,
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
'role' => $request->user()->role, // Memastikan role terkirim dengan jelas
|
||||||
|
// Jangan kirim password_hash atau data sensitif lainnya di sini
|
||||||
|
] : null,
|
||||||
],
|
],
|
||||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
'sidebarOpen' => !$request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||||
'flash' => [
|
'flash' => [
|
||||||
'success' => fn () => $request->session()->get('success'),
|
'success' => fn() => $request->session()->get('success'),
|
||||||
'error' => fn () => $request->session()->get('error'),
|
'error' => fn() => $request->session()->get('error'),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class RoleMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param Closure(Request): (Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Classification extends Model
|
||||||
|
{
|
||||||
|
//
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'image_path',
|
||||||
|
'result',
|
||||||
|
'confidence'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relasi ke User (Satu klasifikasi dimiliki satu User)
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
|
|
||||||
#[Fillable(['name', 'email', 'password'])]
|
#[Fillable(['name', 'email', 'password', 'role'])]
|
||||||
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use App\Http\Middleware\HandleAppearance;
|
use App\Http\Middleware\HandleAppearance;
|
||||||
use App\Http\Middleware\HandleInertiaRequests;
|
use App\Http\Middleware\HandleInertiaRequests;
|
||||||
|
use App\Http\Middleware\RoleMiddleware;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
@ -9,13 +10,17 @@
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__ . '/../routes/web.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__ . '/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
||||||
|
|
||||||
|
$middleware->alias([
|
||||||
|
'role' => RoleMiddleware::class,
|
||||||
|
]);
|
||||||
|
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
HandleAppearance::class,
|
HandleAppearance::class,
|
||||||
HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ public function up(): void
|
||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
|
|
||||||
|
$table->string('role')->default('user');
|
||||||
|
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?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::create('classifications', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
// user_id boleh kosong (untuk guest)
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade');
|
||||||
|
$table->string('image_path');
|
||||||
|
$table->string('result');
|
||||||
|
$table->float('confidence');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('classifications');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
|
@ -16,8 +17,10 @@ public function run(): void
|
||||||
// User::factory(10)->create();
|
// User::factory(10)->create();
|
||||||
|
|
||||||
User::factory()->create([
|
User::factory()->create([
|
||||||
'name' => 'Test User',
|
'name' => 'Admin',
|
||||||
'email' => 'test@example.com',
|
'email' => 'admin@example.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => 'super_user',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"@inertiajs/vue3": "^3.0.0",
|
"@inertiajs/vue3": "^3.0.0",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"laravel-vite-plugin": "^3.0.0",
|
"laravel-vite-plugin": "^3.0.0",
|
||||||
|
|
@ -18,6 +19,7 @@
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-input-otp": "^0.3.2",
|
"vue-input-otp": "^0.3.2",
|
||||||
"vue-sonner": "^2.0.0",
|
"vue-sonner": "^2.0.0",
|
||||||
"ziggy-js": "^2.6.2"
|
"ziggy-js": "^2.6.2"
|
||||||
|
|
@ -575,6 +577,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
|
|
@ -2505,6 +2513,19 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/class-variance-authority": {
|
"node_modules/class-variance-authority": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||||
|
|
@ -6625,6 +6646,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-chartjs": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-eslint-parser": {
|
"node_modules/vue-eslint-parser": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"@inertiajs/vue3": "^3.0.0",
|
"@inertiajs/vue3": "^3.0.0",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"laravel-vite-plugin": "^3.0.0",
|
"laravel-vite-plugin": "^3.0.0",
|
||||||
|
|
@ -46,6 +47,7 @@
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-input-otp": "^0.3.2",
|
"vue-input-otp": "^0.3.2",
|
||||||
"vue-sonner": "^2.0.0",
|
"vue-sonner": "^2.0.0",
|
||||||
"ziggy-js": "^2.6.2"
|
"ziggy-js": "^2.6.2"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@inertiajs/vue3';
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
|
import { computed } from 'vue';
|
||||||
import { route } from 'ziggy-js';
|
import { route } from 'ziggy-js';
|
||||||
import { BookOpen, FolderGit2, LayoutGrid, Users } from 'lucide-vue-next';
|
import { BookOpen, FolderGit2, LayoutGrid, Users } from 'lucide-vue-next';
|
||||||
import AppLogo from '@/components/AppLogo.vue';
|
import AppLogo from '@/components/AppLogo.vue';
|
||||||
import NavFooter from '@/components/NavFooter.vue';
|
import NavFooter from '@/components/NavFooter.vue';
|
||||||
import NavMain from '@/components/NavMain.vue';
|
import NavMain from '@/components/NavMain.vue';
|
||||||
import NavUser from '@/components/NavUser.vue';
|
import NavUser from '@/components/NavUser.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
|
|
@ -17,19 +19,35 @@ import {
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import type { NavItem } from '@/types';
|
import type { NavItem } from '@/types';
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
const page = usePage();
|
||||||
|
// Ambil data user dari auth yang dikirim via HandleInertiaRequests
|
||||||
|
const user = computed(() => page.props.auth.user);
|
||||||
|
|
||||||
|
// 1. Definisikan semua menu, tambahkan properti 'role' untuk menu yang dibatasi
|
||||||
|
const allNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
href: route('admin.dashboard'),
|
href: route('admin.dashboard'),
|
||||||
icon: LayoutGrid,
|
icon: LayoutGrid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Users',
|
title: 'Users Management',
|
||||||
href: route('admin.users.index'),
|
href: route('admin.users.index'),
|
||||||
icon: Users,
|
icon: Users,
|
||||||
|
role: 'admin', // Hanya untuk admin
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 2. Filter menu berdasarkan role user yang sedang login
|
||||||
|
const filteredNavItems = computed(() => {
|
||||||
|
return allNavItems.filter(item => {
|
||||||
|
// Jika menu tidak punya batasan role, tampilkan untuk semua
|
||||||
|
if (!item.role) return true;
|
||||||
|
// Jika ada batasan role, cek apakah role user cocok (admin === admin)
|
||||||
|
return item.role === user.value?.role;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const footerNavItems: NavItem[] = [
|
const footerNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Repository',
|
title: 'Repository',
|
||||||
|
|
@ -59,7 +77,7 @@ const footerNavItems: NavItem[] = [
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain :items="mainNavItems" />
|
<NavMain :items="filteredNavItems" />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,16 @@
|
||||||
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
|
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
|
||||||
import Notification from './Notification.vue';
|
import Notification from './Notification.vue';
|
||||||
import type { BreadcrumbItem } from '@/types';
|
import type { BreadcrumbItem } from '@/types';
|
||||||
|
import { usePage } from '@inertiajs/vue3';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const { breadcrumbs = [] } = defineProps<{
|
const { breadcrumbs = [] } = defineProps<{
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// Mengambil data user untuk keperluan pengecekan role jika dibutuhkan di level ini
|
||||||
|
const page = usePage();
|
||||||
|
const user = computed(() => page.props.auth.user);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { route } from 'ziggy-js';
|
import { route } from 'ziggy-js';
|
||||||
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { User } from '@/types';
|
import { User } from '@/types';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
// Import untuk Chart
|
||||||
|
import { Pie } from 'vue-chartjs';
|
||||||
|
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
|
||||||
|
|
||||||
|
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
layout: {
|
layout: {
|
||||||
|
|
@ -17,29 +22,108 @@ defineOptions({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { user } = defineProps<{ user: User }>();
|
// Terima Props dari Controller
|
||||||
|
const props = defineProps<{
|
||||||
|
user: User,
|
||||||
|
chartData: Array<{ result: string, total: number }>,
|
||||||
|
recentHistory: Array<any>,
|
||||||
|
totalScan: number
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Konfigurasi Warna & Data untuk Pie Chart
|
||||||
|
const chartConfig = computed(() => ({
|
||||||
|
labels: props.chartData.map(item => item.result),
|
||||||
|
datasets: [{
|
||||||
|
backgroundColor: ['#eab308', '#22c55e', '#3b82f6'], // Honey (Yellow), Natural (Green), Washed (Blue)
|
||||||
|
data: props.chartData.map(item => item.total),
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Dashboard" />
|
<Head title="Dashboard" />
|
||||||
|
|
||||||
<div
|
<div class="flex h-full flex-1 flex-col gap-4 p-4 overflow-y-auto">
|
||||||
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4"
|
|
||||||
>
|
<Card class="border-border bg-card">
|
||||||
<!-- card greetings -->
|
<CardHeader class="pb-3">
|
||||||
<Card class="flex-1 border-border bg-card">
|
<CardTitle class="text-lg font-semibold text-foreground">
|
||||||
<div class="flex flex-col">
|
Selamat Datang, {{ user.name }}! ☕
|
||||||
<div class="flex items-center justify-between">
|
</CardTitle>
|
||||||
<div>
|
<CardDescription>
|
||||||
<h3 class="text-base font-medium text-foreground">
|
Anda masuk sebagai <span class="font-bold uppercase">{{ user.role }}</span>. Berikut ringkasan data klasifikasi kopi.
|
||||||
Selamat Datang {{ user.name }}
|
</CardDescription>
|
||||||
</h3>
|
</CardHeader>
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
ini adalah dashboard admin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle class="text-sm font-medium">Total Scan</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="text-2xl font-bold">{{ totalScan }}</div>
|
||||||
|
<p class="text-xs text-muted-foreground">Akumulasi seluruh pemindaian</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||||
|
<Card class="col-span-1 lg:col-span-3">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribusi Pasca Panen</CardTitle>
|
||||||
|
<CardDescription>Persentase hasil klasifikasi</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="h-[300px]">
|
||||||
|
<Pie v-if="chartData.length > 0" :data="chartConfig" :options="chartOptions" />
|
||||||
|
<div v-else class="flex h-full items-center justify-center text-muted-foreground">
|
||||||
|
Belum ada data untuk ditampilkan
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="col-span-1 lg:col-span-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Riwayat Terakhir</CardTitle>
|
||||||
|
<CardDescription>10 pemindaian terbaru</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="relative w-full overflow-auto">
|
||||||
|
<table class="w-full caption-bottom text-sm">
|
||||||
|
<thead class="[&_tr]:border-b">
|
||||||
|
<tr class="border-b transition-colors hover:bg-muted/50">
|
||||||
|
<th class="h-12 px-4 text-left align-middle font-medium">Tanggal</th>
|
||||||
|
<th class="h-12 px-4 text-left align-middle font-medium">Hasil</th>
|
||||||
|
<th class="h-12 px-4 text-left align-middle font-medium">Akurasi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="[&_tr:last-child]:border-0">
|
||||||
|
<tr v-for="item in recentHistory" :key="item.id" class="border-b transition-colors hover:bg-muted/50">
|
||||||
|
<td class="p-4 align-middle">
|
||||||
|
{{ new Date(item.created_at).toLocaleDateString('id-ID') }}
|
||||||
|
</td>
|
||||||
|
<td class="p-4 align-middle font-medium uppercase">{{ item.result }}</td>
|
||||||
|
<td class="p-4 align-middle">{{ item.confidence }}%</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="recentHistory.length === 0">
|
||||||
|
<td colspan="3" class="p-4 text-center text-muted-foreground">Tidak ada data.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
use App\Http\Controllers\Admin\UserController;
|
use App\Http\Controllers\Admin\UserController;
|
||||||
use App\Http\Controllers\Admin\DashboardController;
|
use App\Http\Controllers\Admin\DashboardController;
|
||||||
|
use App\Http\Controllers\Public\ClassificationController; // Import Controller
|
||||||
|
|
||||||
Route::redirect('dashboard', 'admin/dashboard')->name('dashboard');
|
Route::redirect('dashboard', 'admin/dashboard')->name('dashboard');
|
||||||
|
|
||||||
|
|
@ -13,9 +14,14 @@
|
||||||
]);
|
]);
|
||||||
})->name('home');
|
})->name('home');
|
||||||
|
|
||||||
|
Route::post('/classifications', [ClassificationController::class, 'store'])->name('public.classifications.store');
|
||||||
|
|
||||||
Route::prefix('admin')->name('admin.')->middleware(['auth', 'verified'])->group(function () {
|
Route::prefix('admin')->name('admin.')->middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||||
Route::resource('users', UserController::class)->names('users');
|
Route::resource('users', UserController::class)->names('users');
|
||||||
|
|
||||||
|
// Route tambahan untuk Admin jika ingin melihat history klasifikasi secara global
|
||||||
|
// Route::get('history', [DashboardController::class, 'history'])->name('history');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue