Feat: Roles management fiture
This commit is contained in:
parent
ae9e0a2eb0
commit
6255b83d95
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,17 @@ public function version(Request $request): ?string
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public function share(Request $request): array
|
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',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -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']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
export function AppSidebar() {
|
||||||
|
const { auth } = usePage().props as any;
|
||||||
|
const userRole = auth.user.role;
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
Loading…
Reference in New Issue