Feat: Role Middleware with CRUD Manajemen Pengguna
This commit is contained in:
parent
6255b83d95
commit
9fb75ac5cd
|
|
@ -1,32 +1,68 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
class UserController extends Controller
|
use Inertia\Inertia;
|
||||||
{
|
|
||||||
public function index()
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/users/index', [
|
public function index()
|
||||||
'users' => User::select('id', 'name', 'email', 'role', 'created_at')
|
{
|
||||||
->latest()
|
return Inertia::render('admin/users/index', [
|
||||||
->get()
|
'users' => User::select('id', 'name', 'email', 'role', 'created_at')
|
||||||
]);
|
->latest()
|
||||||
}
|
->get(),
|
||||||
|
]);
|
||||||
public function update(Request $request, User $user)
|
}
|
||||||
{
|
|
||||||
// Validasi perpindahan role
|
public function create()
|
||||||
$validated = $request->validate([
|
{
|
||||||
'role' => 'required|in:admin,employee',
|
return Inertia::render('admin/users/create');
|
||||||
]);
|
}
|
||||||
|
|
||||||
$user->update($validated);
|
public function store(Request $request)
|
||||||
|
{
|
||||||
return back()->with('success', "Role {$user->name} berhasil diubah.");
|
$validated = $request->validate([
|
||||||
}
|
'name' => 'required|string|max:255',
|
||||||
}
|
'email' => 'required|email|max:255|unique:users,email',
|
||||||
|
'password' => 'required|string|min:8|confirmed',
|
||||||
|
'role' => ['required', Rule::in(['admin', 'employee'])],
|
||||||
|
], [
|
||||||
|
'name.required' => 'Nama wajib diisi.',
|
||||||
|
'email.required' => 'Email wajib diisi.',
|
||||||
|
'email.email' => 'Format email tidak valid.',
|
||||||
|
'email.unique' => 'Email sudah terdaftar.',
|
||||||
|
'password.required' => 'Password wajib diisi.',
|
||||||
|
'password.min' => 'Password minimal 8 karakter.',
|
||||||
|
'password.confirmed' => 'Konfirmasi password tidak cocok.',
|
||||||
|
'role.required' => 'Role wajib dipilih.',
|
||||||
|
'role.in' => 'Role hanya boleh admin atau employee.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
User::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
'role' => $validated['role'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.users.index')
|
||||||
|
->with('success', "User {$validated['name']} berhasil ditambahkan.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, User $user)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'role' => ['required', Rule::in(['admin', 'employee'])],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->update($validated);
|
||||||
|
|
||||||
|
return back()->with('success', "Role {$user->name} berhasil diubah.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ class DashboardController extends Controller
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if (auth()->user()->role !== 'admin') {
|
if (auth()->user()->role !== 'admin') {
|
||||||
return redirect()->route('employee.dashboard');
|
return redirect()->route('employee.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
$stats = [
|
$stats = [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Responses;
|
||||||
|
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
|
||||||
|
|
||||||
|
class LoginResponse implements LoginResponseContract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Redirect user based on their role after a successful login.
|
||||||
|
*/
|
||||||
|
public function toResponse($request): RedirectResponse|JsonResponse
|
||||||
|
{
|
||||||
|
$role = $request->user()->role;
|
||||||
|
|
||||||
|
$redirectUrl = match ($role) {
|
||||||
|
'admin' => route('dashboard'),
|
||||||
|
'employee' => route('employee.index'),
|
||||||
|
default => route('dashboard'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($request->wantsJson()) {
|
||||||
|
return new JsonResponse(['two_factor' => false], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended($redirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Http\Responses\LoginResponse;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Support\Facades\Date;
|
use Illuminate\Support\Facades\Date;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Validation\Rules\Password;
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -15,7 +17,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
//
|
$this->app->singleton(LoginResponseContract::class, LoginResponse::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,34 @@ class MasterDataSeeder extends Seeder
|
||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
|
// === DEPARTMENTS ===
|
||||||
$depts = [
|
$depts = [
|
||||||
['name' => 'Information Technology', 'description' => 'Bagian IT & Development'],
|
['name' => 'Information Technology', 'description' => 'Bagian IT & Development'],
|
||||||
['name' => 'Human Resources', 'description' => 'Bagian Kepegawaian'],
|
['name' => 'Human Resources', 'description' => 'Bagian Kepegawaian'],
|
||||||
['name' => 'Finance', 'description' => 'Bagian Keuangan'],
|
['name' => 'Finance', 'description' => 'Bagian Keuangan & Akuntansi'],
|
||||||
|
['name' => 'Marketing', 'description' => 'Bagian Pemasaran & Komunikasi'],
|
||||||
|
['name' => 'Operations', 'description' => 'Bagian Operasional & Logistik'],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($depts as $dept) {
|
foreach ($depts as $dept) {
|
||||||
Department::create($dept);
|
Department::create($dept);
|
||||||
}
|
}
|
||||||
|
|
||||||
$positions = ['Manager', 'Senior Developer', 'Staff Admin', 'HR Specialist'];
|
// === POSITIONS ===
|
||||||
|
$positions = [
|
||||||
|
'Manager',
|
||||||
|
'Senior Developer',
|
||||||
|
'Junior Developer',
|
||||||
|
'Staff Admin',
|
||||||
|
'HR Specialist',
|
||||||
|
'Finance Analyst',
|
||||||
|
'Marketing Staff',
|
||||||
|
'Operations Staff',
|
||||||
|
];
|
||||||
|
|
||||||
foreach ($positions as $pos) {
|
foreach ($positions as $pos) {
|
||||||
Position::create(['name' => $pos]);
|
Position::create(['name' => $pos]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,18 +12,60 @@ public function run(): void
|
||||||
{
|
{
|
||||||
// Akun Admin Utama
|
// Akun Admin Utama
|
||||||
User::create([
|
User::create([
|
||||||
'name' => 'admin',
|
'name' => 'admin',
|
||||||
'email' => 'admin@gmail.com',
|
'email' => 'admin@gmail.com',
|
||||||
'password' => Hash::make('password'),
|
'password' => Hash::make('password'),
|
||||||
'role' => 'admin',
|
'role' => 'admin',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Akun Contoh Karyawan
|
|
||||||
User::create([
|
User::create([
|
||||||
'name' => 'Jono Joni',
|
'name' => 'Jono Joni',
|
||||||
'email' => 'jono@gmail.com',
|
'email' => 'jono@gmail.com',
|
||||||
'password' => Hash::make('password'),
|
'password' => Hash::make('password'),
|
||||||
'role' => 'employee',
|
'role' => 'employee',
|
||||||
]);
|
]);
|
||||||
|
User::create([
|
||||||
|
'name' => 'Siti Nurhaliza',
|
||||||
|
'email' => 'siti.admin@hris.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => 'admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- Akun Karyawan (Employee) ---
|
||||||
|
User::create([
|
||||||
|
'name' => 'Budi Santoso',
|
||||||
|
'email' => 'budi.santoso@hris.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => 'employee',
|
||||||
|
]);
|
||||||
|
|
||||||
|
User::create([
|
||||||
|
'name' => 'Dewi Rahayu',
|
||||||
|
'email' => 'dewi.rahayu@hris.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => 'employee',
|
||||||
|
]);
|
||||||
|
|
||||||
|
User::create([
|
||||||
|
'name' => 'Rizky Firmansyah',
|
||||||
|
'email' => 'rizky.firmansyah@hris.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => 'employee',
|
||||||
|
]);
|
||||||
|
|
||||||
|
User::create([
|
||||||
|
'name' => 'Anisa Putri',
|
||||||
|
'email' => 'anisa.putri@hris.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => 'employee',
|
||||||
|
]);
|
||||||
|
|
||||||
|
User::create([
|
||||||
|
'name' => 'Hendra Kurniawan',
|
||||||
|
'email' => 'hendra.kurniawan@hris.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => 'employee',
|
||||||
|
]);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Link, usePage } from '@inertiajs/react'; // Tambahkan usePage
|
import { Link, usePage } from '@inertiajs/react';
|
||||||
import { LayoutGrid, Users, Briefcase, Building2, SquareUser } from 'lucide-react';
|
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';
|
||||||
|
|
@ -13,39 +13,39 @@ import {
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { dashboard } from '@/routes';
|
import { dashboard } from '@/routes';
|
||||||
import type { NavItem } from '@/types';
|
import type { NavItem, SharedData } from '@/types';
|
||||||
import AppLogo from './app-logo';
|
import AppLogo from './app-logo';
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const { auth } = usePage().props as any;
|
const { auth } = usePage<SharedData>().props;
|
||||||
const userRole = auth.user.role;
|
const userRole = auth.user.role;
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
const mainNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
href: userRole === 'admin' ? '/dashboard' : '/employee/dashboard',
|
href: userRole === 'admin' ? '/dashboard' : '/employee/index',
|
||||||
icon: LayoutGrid,
|
icon: LayoutGrid,
|
||||||
},
|
},
|
||||||
...(userRole === 'admin' ? [
|
...(userRole === 'admin' ? [
|
||||||
{
|
{
|
||||||
title: 'Manajemen Karyawan',
|
title: 'Manajemen Karyawan',
|
||||||
href: '/admin/employees',
|
href: '/admin/employees',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Manajemen Departemen',
|
title: 'Manajemen Departemen',
|
||||||
href: '/admin/departments',
|
href: '/admin/departments',
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Manajemen Jabatan',
|
title: 'Manajemen Jabatan',
|
||||||
href: '/admin/positions',
|
href: '/admin/positions',
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Manajemen Pengguna',
|
title: 'Manajemen Pengguna',
|
||||||
href: '/admin/users',
|
href: '/admin/users',
|
||||||
icon: SquareUser,
|
icon: SquareUser,
|
||||||
},
|
},
|
||||||
] : []),
|
] : []),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { Head, Link, useForm } from '@inertiajs/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
import { type BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
|
{ title: 'Manajemen Pengguna', href: '/admin/users' },
|
||||||
|
{ title: 'Tambah User', href: '/admin/users/create' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Create() {
|
||||||
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
password_confirmation: '',
|
||||||
|
role: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
post('/admin/users');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout breadcrumbs={breadcrumbs}>
|
||||||
|
<Head title="Tambah User Baru" />
|
||||||
|
|
||||||
|
<div className="p-4 md:p-8 w-full">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Tambah User Baru</h2>
|
||||||
|
<p className="text-muted-foreground">Buat akun pengguna baru beserta hak aksesnya.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/admin/users">Kembali</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Form User</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<form onSubmit={submit} className="space-y-6">
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{/* Nama */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">
|
||||||
|
Nama Lengkap <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder="Contoh: Budi Santoso"
|
||||||
|
value={data.name}
|
||||||
|
onChange={(e) => setData('name', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">
|
||||||
|
Email <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Contoh: budi@hris.com"
|
||||||
|
value={data.email}
|
||||||
|
onChange={(e) => setData('email', e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.email}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">
|
||||||
|
Password <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Minimal 8 karakter"
|
||||||
|
value={data.password}
|
||||||
|
onChange={(e) => setData('password', e.target.value)}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.password}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Konfirmasi Password */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password_confirmation">
|
||||||
|
Konfirmasi Password <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="password_confirmation"
|
||||||
|
type="password"
|
||||||
|
placeholder="Ulangi password di atas"
|
||||||
|
value={data.password_confirmation}
|
||||||
|
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
{errors.password_confirmation && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.password_confirmation}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="role">
|
||||||
|
Role / Hak Akses <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={data.role}
|
||||||
|
onValueChange={(val) => setData('role', val)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="role" className="w-full">
|
||||||
|
<SelectValue placeholder="Pilih role..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
<SelectItem value="employee">Employee</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.role && (
|
||||||
|
<p className="text-red-500 text-xs">{errors.role}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4 pt-4">
|
||||||
|
<Button type="button" variant="ghost" asChild>
|
||||||
|
<Link href="/admin/users">Batal</Link>
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={processing}>
|
||||||
|
{processing ? 'Menyimpan...' : 'Simpan User'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,61 +1,133 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Head, useForm } from '@inertiajs/react';
|
import { Head, Link, router, usePage } from '@inertiajs/react';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import AppLayout from '@/layouts/app-layout';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
export default function Index({ users }: { users: any[] }) {
|
import AppLayout from '@/layouts/app-layout';
|
||||||
const { patch, processing } = useForm();
|
import { type BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
const updateRole = (id: number, role: string) => {
|
interface User {
|
||||||
patch(route('admin.users.update', id), {
|
id: number;
|
||||||
data: { role: role },
|
name: string;
|
||||||
preserveScroll: true,
|
email: string;
|
||||||
});
|
role: 'admin' | 'employee';
|
||||||
};
|
created_at: string;
|
||||||
|
}
|
||||||
return (
|
|
||||||
<AppLayout>
|
interface PageProps {
|
||||||
<Head title="Manajemen User" />
|
users: User[];
|
||||||
<div className="p-4 md:p-8 w-full space-y-6">
|
[key: string]: unknown;
|
||||||
<h2 className="text-2xl font-bold">Manajemen Akses User</h2>
|
}
|
||||||
<Card>
|
|
||||||
<CardHeader><CardTitle>Daftar Pengguna</CardTitle></CardHeader>
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
<CardContent className="p-0">
|
{ title: 'Manajemen Pengguna', href: '/admin/users' },
|
||||||
<table className="w-full text-sm text-left">
|
];
|
||||||
<thead className="bg-zinc-50 border-b">
|
|
||||||
<tr>
|
export default function Index({ users }: PageProps) {
|
||||||
<th className="p-4">Nama</th>
|
const { flash } = usePage<any>().props;
|
||||||
<th className="p-4 text-center">Role</th>
|
const [processingId, setProcessingId] = useState<number | null>(null);
|
||||||
<th className="p-4 text-right">Aksi</th>
|
|
||||||
</tr>
|
const updateRole = (id: number, role: string) => {
|
||||||
</thead>
|
setProcessingId(id);
|
||||||
<tbody>
|
router.patch(
|
||||||
{users.map((user) => (
|
`/admin/users/${id}`,
|
||||||
<tr key={user.id} className="border-b">
|
{ role },
|
||||||
<td className="p-4 font-medium">{user.name}</td>
|
{
|
||||||
<td className="p-4 text-center">
|
preserveScroll: true,
|
||||||
<span className="bg-zinc-100 px-2 py-1 rounded text-[10px] font-bold">
|
onFinish: () => setProcessingId(null),
|
||||||
{user.role.toUpperCase()}
|
},
|
||||||
</span>
|
);
|
||||||
</td>
|
};
|
||||||
<td className="p-4 text-right">
|
|
||||||
<Select defaultValue={user.role} onValueChange={(val) => updateRole(user.id, val)} disabled={processing}>
|
return (
|
||||||
<SelectTrigger className="w-[120px] ml-auto"><SelectValue /></SelectTrigger>
|
<AppLayout breadcrumbs={breadcrumbs}>
|
||||||
<SelectContent>
|
<Head title="Manajemen Pengguna" />
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
|
||||||
<SelectItem value="employee">Employee</SelectItem>
|
<div className="p-4 md:p-8 w-full space-y-4">
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
{/* Flash Messages */}
|
||||||
</td>
|
{flash?.success && (
|
||||||
</tr>
|
<div className="p-4 bg-green-50 text-green-700 border border-green-200 rounded-md text-sm">
|
||||||
))}
|
{flash.success}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
)}
|
||||||
</CardContent>
|
{flash?.error && (
|
||||||
</Card>
|
<div className="p-4 bg-red-50 text-red-700 border border-red-200 rounded-md text-sm">
|
||||||
</div>
|
{flash.error}
|
||||||
</AppLayout>
|
</div>
|
||||||
);
|
)}
|
||||||
}
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Manajemen Akses User</h2>
|
||||||
|
<p className="text-muted-foreground">Kelola akun dan hak akses pengguna sistem.</p>
|
||||||
|
</div>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/admin/users/create">+ Tambah User Baru</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabel */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle>Daftar Pengguna</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Separator />
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table className="w-full text-sm text-left">
|
||||||
|
<thead className="text-muted-foreground bg-zinc-50/50">
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="h-12 px-4 font-medium">Nama</th>
|
||||||
|
<th className="h-12 px-4 font-medium">Email</th>
|
||||||
|
<th className="h-12 px-4 font-medium text-center">Role Saat Ini</th>
|
||||||
|
<th className="h-12 px-4 font-medium text-right">Ubah Role</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.length > 0 ? (
|
||||||
|
users.map((user) => (
|
||||||
|
<tr key={user.id} className="border-b hover:bg-zinc-50">
|
||||||
|
<td className="p-4 font-medium">{user.name}</td>
|
||||||
|
<td className="p-4 text-muted-foreground">{user.email}</td>
|
||||||
|
<td className="p-4 text-center">
|
||||||
|
<span className="bg-zinc-100 px-2.5 py-1 rounded-full text-[11px] font-semibold tracking-wide">
|
||||||
|
{user.role.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-right">
|
||||||
|
<Select
|
||||||
|
defaultValue={user.role}
|
||||||
|
onValueChange={(val) => updateRole(user.id, val)}
|
||||||
|
disabled={processingId === user.id}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[130px] ml-auto">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
<SelectItem value="employee">Employee</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="p-8 text-center text-muted-foreground">
|
||||||
|
Belum ada data pengguna.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Head } from '@inertiajs/react';
|
||||||
|
import AppLayout from '@/layouts/app-layout';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { usePage } from '@inertiajs/react';
|
||||||
|
import { User2, Mail, ShieldCheck } from 'lucide-react';
|
||||||
|
import type { SharedData } from '@/types';
|
||||||
|
import type { BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
|
{ title: 'Portal Saya', href: '/employee/index' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EmployeeIndex() {
|
||||||
|
const { auth } = usePage<SharedData>().props;
|
||||||
|
const user = auth.user;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout breadcrumbs={breadcrumbs}>
|
||||||
|
<Head title="Portal Karyawan" />
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col gap-6 p-4 md:p-8">
|
||||||
|
{/* Header Sambutan */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Selamat datang, {user.name}!
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Ini adalah portal SDM Anda. Pantau informasi akun Anda di sini.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kartu Info Akun */}
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Nama Lengkap</CardTitle>
|
||||||
|
<User2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-lg font-semibold">{user.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Nama akun terdaftar</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Alamat Email</CardTitle>
|
||||||
|
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-lg font-semibold truncate">{user.email}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Email untuk login</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Hak Akses</CardTitle>
|
||||||
|
<ShieldCheck className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-2">
|
||||||
|
<Badge variant="secondary" className="w-fit capitalize">
|
||||||
|
{user.role}
|
||||||
|
</Badge>
|
||||||
|
<p className="text-xs text-muted-foreground">Role akun Anda saat ini</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ export type User = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
role: 'admin' | 'employee';
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
email_verified_at: string | null;
|
email_verified_at: string | null;
|
||||||
two_factor_enabled?: boolean;
|
two_factor_enabled?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,16 @@
|
||||||
Route::resource('employees', EmployeeController::class);
|
Route::resource('employees', EmployeeController::class);
|
||||||
|
|
||||||
Route::get('users', [UserController::class, 'index'])->name('users.index');
|
Route::get('users', [UserController::class, 'index'])->name('users.index');
|
||||||
|
Route::get('users/create', [UserController::class, 'create'])->name('users.create');
|
||||||
|
Route::post('users', [UserController::class, 'store'])->name('users.store');
|
||||||
Route::patch('users/{user}', [UserController::class, 'update'])->name('users.update');
|
Route::patch('users/{user}', [UserController::class, 'update'])->name('users.update');
|
||||||
});
|
});
|
||||||
|
|
||||||
// KELOMPOK KARYAWAN
|
// KELOMPOK KARYAWAN
|
||||||
Route::middleware(['auth', 'verified', 'role:employee'])->group(function () {
|
Route::middleware(['auth', 'verified', 'role:employee'])->group(function () {
|
||||||
Route::get('/employee/dashboard', function () {
|
Route::get('/employee/index', function () {
|
||||||
return Inertia::render('employee/dashboard');
|
return Inertia::render('employee/index');
|
||||||
})->name('employee.dashboard');
|
})->name('employee.index');
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
Loading…
Reference in New Issue