Feat: Roles management fiture

This commit is contained in:
IlhamIslamy 2026-03-02 04:41:07 +07:00
parent ae9e0a2eb0
commit 6255b83d95
15 changed files with 280 additions and 81 deletions

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
class UserController extends Controller
{
public function index()
{
return Inertia::render('admin/users/index', [
'users' => User::select('id', 'name', 'email', 'role', 'created_at')
->latest()
->get()
]);
}
public function update(Request $request, User $user)
{
// Validasi perpindahan role
$validated = $request->validate([
'role' => 'required|in:admin,employee',
]);
$user->update($validated);
return back()->with('success', "Role {$user->name} berhasil diubah.");
}
}

View File

@ -12,14 +12,16 @@ class DashboardController extends Controller
{ {
public function index() public function index()
{ {
// Ringkasan Data if (auth()->user()->role !== 'admin') {
return redirect()->route('employee.dashboard');
}
$stats = [ $stats = [
'total_employees' => Employee::count(), 'total_employees' => Employee::count(),
'total_departments' => Department::count(), 'total_departments' => Department::count(),
'total_positions' => Position::count(), 'total_positions' => Position::count(),
]; ];
// Data Grafik Gender (Pie Chart)
$genderData = Employee::selectRaw('gender, count(*) as total') $genderData = Employee::selectRaw('gender, count(*) as total')
->groupBy('gender') ->groupBy('gender')
->get() ->get()
@ -29,7 +31,6 @@ public function index()
'fill' => $item->gender == 'L' ? '#3b82f6' : '#ec4899', 'fill' => $item->gender == 'L' ? '#3b82f6' : '#ec4899',
]); ]);
// Data Grafik Departemen (Bar Chart)
$deptData = Department::withCount('employees') $deptData = Department::withCount('employees')
->having('employees_count', '>', 0) ->having('employees_count', '>', 0)
->get() ->get()
@ -38,7 +39,6 @@ public function index()
'employees' => $item->employees_count, 'employees' => $item->employees_count,
]); ]);
// Karyawan Terbaru (Table)
$latestEmployees = Employee::with(['department', 'position', 'user']) $latestEmployees = Employee::with(['department', 'position', 'user'])
->latest('join_date') ->latest('join_date')
->take(5) ->take(5)

View File

@ -37,11 +37,14 @@ public function share(Request $request): array
{ {
return [ return [
...parent::share($request), ...parent::share($request),
'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,
] : null,
], ],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
]; ];
} }
} }

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RoleMiddleware
{
public function handle(Request $request, Closure $next, string $role): Response
{
if (! $request->user() || $request->user()->role !== $role) {
abort(403, 'Akses Ditolak.');
}
return $next($request);
}
}

View File

@ -21,6 +21,10 @@
HandleInertiaRequests::class, HandleInertiaRequests::class,
AddLinkHeadersForPreloadedAssets::class, AddLinkHeadersForPreloadedAssets::class,
]); ]);
$middleware->alias([
'role' => \App\Http\Middleware\RoleMiddleware::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //

View File

@ -2,37 +2,15 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Department;
use App\Models\Position;
use App\Models\User;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
/**
* Seed the application's database.
*/
public function run(): void public function run(): void
{ {
User::create([ $this->call([
'name' => 'Super Admin', UserSeeder::class,
'email' => 'admin@zhanhui.com', MasterDataSeeder::class,
'password' => Hash::make('password123'),
'role' => 'admin',
]); ]);
// Seed departments
Department::create(['name' => 'Information Technology', 'description' => 'Bagian IT & Development']);
Department::create(['name' => 'Human Resources', 'description' => 'Bagian Kepegawaian']);
Department::create(['name' => 'Finance', 'description' => 'Bagian Keuangan']);
Department::create(['name' => 'Marketing', 'description' => 'Bagian Pemasaran']);
// Seed positions
Position::create(['name' => 'General Manager']);
Position::create(['name' => 'Manager']);
Position::create(['name' => 'Supervisor']);
Position::create(['name' => 'Staff']);
Position::create(['name' => 'Intern']);
} }
} }

View File

@ -0,0 +1,29 @@
<?php
namespace Database\Seeders;
use App\Models\Department;
use App\Models\Position;
use Illuminate\Database\Seeder;
class MasterDataSeeder extends Seeder
{
public function run(): void
{
$depts = [
['name' => 'Information Technology', 'description' => 'Bagian IT & Development'],
['name' => 'Human Resources', 'description' => 'Bagian Kepegawaian'],
['name' => 'Finance', 'description' => 'Bagian Keuangan'],
];
foreach ($depts as $dept) {
Department::create($dept);
}
$positions = ['Manager', 'Senior Developer', 'Staff Admin', 'HR Specialist'];
foreach ($positions as $pos) {
Position::create(['name' => $pos]);
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run(): void
{
// Akun Admin Utama
User::create([
'name' => 'admin',
'email' => 'admin@gmail.com',
'password' => Hash::make('password'),
'role' => 'admin',
]);
// Akun Contoh Karyawan
User::create([
'name' => 'Jono Joni',
'email' => 'jono@gmail.com',
'password' => Hash::make('password'),
'role' => 'employee',
]);
}
}

View File

@ -1,5 +1,6 @@
import { Link } from '@inertiajs/react'; /* eslint-disable @typescript-eslint/no-explicit-any */
import { LayoutGrid, Users, Briefcase, Building2} from 'lucide-react'; import { Link, usePage } from '@inertiajs/react'; // Tambahkan usePage
import { LayoutGrid, Users, Briefcase, Building2, SquareUser } from 'lucide-react';
import { NavMain } from '@/components/nav-main'; import { NavMain } from '@/components/nav-main';
import { NavUser } from '@/components/nav-user'; import { NavUser } from '@/components/nav-user';
import { import {
@ -15,12 +16,17 @@ import { dashboard } from '@/routes';
import type { NavItem } from '@/types'; import type { NavItem } from '@/types';
import AppLogo from './app-logo'; import AppLogo from './app-logo';
export function AppSidebar() {
const { auth } = usePage().props as any;
const userRole = auth.user.role;
const mainNavItems: NavItem[] = [ const mainNavItems: NavItem[] = [
{ {
title: 'Dashboard', title: 'Dashboard',
href: '/dashboard', href: userRole === 'admin' ? '/dashboard' : '/employee/dashboard',
icon: LayoutGrid, icon: LayoutGrid,
}, },
...(userRole === 'admin' ? [
{ {
title: 'Manajemen Karyawan', title: 'Manajemen Karyawan',
href: '/admin/employees', href: '/admin/employees',
@ -36,28 +42,21 @@ const mainNavItems: NavItem[] = [
href: '/admin/positions', href: '/admin/positions',
icon: Briefcase, icon: Briefcase,
}, },
{
title: 'Manajemen Pengguna',
href: '/admin/users',
icon: SquareUser,
},
] : []),
]; ];
// const footerNavItems: NavItem[] = [
// {
// title: 'Repository',
// href: 'https://github.com/laravel/react-starter-kit',
// icon: Folder,
// },
// {
// title: 'Documentation',
// href: 'https://laravel.com/docs/starter-kits#react',
// icon: BookOpen,
// },
// ];
export function AppSidebar() {
return ( return (
<Sidebar collapsible="icon" variant="inset"> <Sidebar collapsible="icon" variant="inset">
<SidebarHeader> <SidebarHeader>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton size="lg" asChild> <SidebarMenuButton size="lg" asChild>
<Link href={dashboard()} prefetch> <Link href={dashboard()} prefetch>
<AppLogo /> <AppLogo />
</Link> </Link>
@ -71,7 +70,6 @@ export function AppSidebar() {
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
{/* <NavFooter items={footerNavItems} className="mt-auto" /> */}
<NavUser /> <NavUser />
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>

View File

@ -0,0 +1,61 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Head, useForm } from '@inertiajs/react';
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import AppLayout from '@/layouts/app-layout';
export default function Index({ users }: { users: any[] }) {
const { patch, processing } = useForm();
const updateRole = (id: number, role: string) => {
patch(route('admin.users.update', id), {
data: { role: role },
preserveScroll: true,
});
};
return (
<AppLayout>
<Head title="Manajemen User" />
<div className="p-4 md:p-8 w-full space-y-6">
<h2 className="text-2xl font-bold">Manajemen Akses User</h2>
<Card>
<CardHeader><CardTitle>Daftar Pengguna</CardTitle></CardHeader>
<CardContent className="p-0">
<table className="w-full text-sm text-left">
<thead className="bg-zinc-50 border-b">
<tr>
<th className="p-4">Nama</th>
<th className="p-4 text-center">Role</th>
<th className="p-4 text-right">Aksi</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id} className="border-b">
<td className="p-4 font-medium">{user.name}</td>
<td className="p-4 text-center">
<span className="bg-zinc-100 px-2 py-1 rounded text-[10px] font-bold">
{user.role.toUpperCase()}
</span>
</td>
<td className="p-4 text-right">
<Select defaultValue={user.role} onValueChange={(val) => updateRole(user.id, val)} disabled={processing}>
<SelectTrigger className="w-[120px] ml-auto"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="employee">Employee</SelectItem>
</SelectContent>
</Select>
</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,33 @@
import { Head, usePage } from '@inertiajs/react';
import AppLayout from '@/layouts/app-layout';
export default function Dashboard() {
const { auth } = usePage().props as any;
return (
<AppLayout>
<Head title="Portal Karyawan" />
<div className="flex flex-1 flex-col gap-4 p-4 md:p-8">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold">Halo, {auth.user.name}!</h1>
<p className="text-muted-foreground italic">
Selamat datang di portal SDM. Kamu login sebagai <span className="font-bold text-foreground underline">{auth.user.role}</span>.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Kotak-kotak info simpel untuk karyawan */}
<div className="p-6 bg-white border rounded-xl shadow-sm space-y-2">
<p className="text-xs font-bold text-zinc-400 uppercase">Email</p>
<p className="font-medium">{auth.user.email}</p>
</div>
<div className="p-6 bg-white border rounded-xl shadow-sm space-y-2">
<p className="text-xs font-bold text-zinc-400 uppercase">Status Akun</p>
<p className="font-medium text-green-600">Aktif</p>
</div>
</div>
</div>
</AppLayout>
);
}

View File

@ -3,6 +3,7 @@
use App\Http\Controllers\Admin\DepartmentController; use App\Http\Controllers\Admin\DepartmentController;
use App\Http\Controllers\Admin\EmployeeController; use App\Http\Controllers\Admin\EmployeeController;
use App\Http\Controllers\Admin\PositionController; use App\Http\Controllers\Admin\PositionController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Inertia\Inertia; use Inertia\Inertia;
@ -18,9 +19,21 @@
->middleware(['auth', 'verified']) ->middleware(['auth', 'verified'])
->name('dashboard'); ->name('dashboard');
Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(function () { // ADMIN
Route::middleware(['auth', 'verified', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
Route::resource('departments', DepartmentController::class); Route::resource('departments', DepartmentController::class);
Route::resource('positions', PositionController::class); Route::resource('positions', PositionController::class);
Route::resource('employees', EmployeeController::class); Route::resource('employees', EmployeeController::class);
Route::get('users', [UserController::class, 'index'])->name('users.index');
Route::patch('users/{user}', [UserController::class, 'update'])->name('users.update');
}); });
// KELOMPOK KARYAWAN
Route::middleware(['auth', 'verified', 'role:employee'])->group(function () {
Route::get('/employee/dashboard', function () {
return Inertia::render('employee/dashboard');
})->name('employee.dashboard');
});
require __DIR__.'/settings.php'; require __DIR__.'/settings.php';