feat: implement Role management system with CRUD operations, including role creation, editing, and deletion, along with permission assignment
This commit is contained in:
parent
0fdd02e9e4
commit
96ad0de44c
112
app/Http/Controllers/Admin/System/RoleController.php
Normal file
112
app/Http/Controllers/Admin/System/RoleController.php
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\System;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\System\RoleRequest;
|
||||||
|
use App\Support\LogHelper;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class RoleController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/system/role/index', [
|
||||||
|
'roles' => Role::with('permissions')->latest()->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/system/role/create', [
|
||||||
|
'permissions' => Permission::all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(RoleRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($validated) {
|
||||||
|
$role = Role::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'guard_name' => $validated['guard_name'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$role->syncPermissions($validated['permissions'] ?? []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()->route('system.role.index')->with('success', 'Data berhasil disimpan');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
LogHelper::logException($e, 'Failed to store role', [
|
||||||
|
'name' => $validated['name'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Terjadi kesalahan, silakan hubungi pengembang');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Role $role): Response
|
||||||
|
{
|
||||||
|
$role->load('permissions');
|
||||||
|
|
||||||
|
return Inertia::render('admin/system/role/edit', [
|
||||||
|
'role' => $role,
|
||||||
|
'permissions' => Permission::all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(RoleRequest $request, Role $role): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($role, $validated) {
|
||||||
|
$role->update([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'guard_name' => $validated['guard_name'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$role->syncPermissions($validated['permissions'] ?? []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()->route('system.role.index')->with('success', 'Data berhasil diperbarui');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
LogHelper::logException($e, 'Failed to update role', [
|
||||||
|
'role_id' => $role->id,
|
||||||
|
'name' => $validated['name'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Terjadi kesalahan, silakan hubungi pengembang');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Role $role): RedirectResponse
|
||||||
|
{
|
||||||
|
$role->delete();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Data berhasil dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bulkDestroy(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$ids = $request->input('ids');
|
||||||
|
|
||||||
|
Role::whereIn('id', $ids)->delete();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Data terpilih berhasil dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Requests/Admin/System/RoleRequest.php
Normal file
31
app/Http/Requests/Admin/System/RoleRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\System;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class RoleRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$roleId = $this->route('role')?->id;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('roles', 'name')->ignore($roleId),
|
||||||
|
],
|
||||||
|
'guard_name' => ['required', 'string'],
|
||||||
|
'permissions' => ['nullable', 'array'],
|
||||||
|
'permissions.*' => ['nullable', Rule::exists('permissions', 'id')],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -82,6 +82,12 @@ public function run(): void
|
|||||||
Permission::firstOrCreate(['name' => 'View:Setting', 'guard_name' => 'web']);
|
Permission::firstOrCreate(['name' => 'View:Setting', 'guard_name' => 'web']);
|
||||||
Permission::firstOrCreate(['name' => 'Edit:Setting', 'guard_name' => 'web']);
|
Permission::firstOrCreate(['name' => 'Edit:Setting', 'guard_name' => 'web']);
|
||||||
|
|
||||||
|
// Permissions Role
|
||||||
|
Permission::firstOrCreate(['name' => 'View:Role', 'guard_name' => 'web']);
|
||||||
|
Permission::firstOrCreate(['name' => 'Create:Role', 'guard_name' => 'web']);
|
||||||
|
Permission::firstOrCreate(['name' => 'Edit:Role', 'guard_name' => 'web']);
|
||||||
|
Permission::firstOrCreate(['name' => 'Delete:Role', 'guard_name' => 'web']);
|
||||||
|
|
||||||
// Permissions Log
|
// Permissions Log
|
||||||
Permission::firstOrCreate(['name' => 'View:Log', 'guard_name' => 'web']);
|
Permission::firstOrCreate(['name' => 'View:Log', 'guard_name' => 'web']);
|
||||||
|
|
||||||
@ -98,7 +104,14 @@ public function run(): void
|
|||||||
$developer->syncPermissions($allPermissions);
|
$developer->syncPermissions($allPermissions);
|
||||||
|
|
||||||
// Assign permissions to Owner (All except View:Log)
|
// Assign permissions to Owner (All except View:Log)
|
||||||
$ownerPermissions = Permission::whereNotIn('name', ['View:Log'])->get();
|
$ownerPermissions = Permission::whereNotIn('name', [
|
||||||
|
'View:Log',
|
||||||
|
|
||||||
|
'View:Role',
|
||||||
|
'Create:Role',
|
||||||
|
'Edit:Role',
|
||||||
|
'Delete:Role',
|
||||||
|
])->get();
|
||||||
$owner->syncPermissions($ownerPermissions);
|
$owner->syncPermissions($ownerPermissions);
|
||||||
|
|
||||||
// Assign permissions to Admin
|
// Assign permissions to Admin
|
||||||
@ -138,6 +151,11 @@ public function run(): void
|
|||||||
'View:Setting',
|
'View:Setting',
|
||||||
'Edit:Setting',
|
'Edit:Setting',
|
||||||
|
|
||||||
|
'View:Role',
|
||||||
|
'Create:Role',
|
||||||
|
'Edit:Role',
|
||||||
|
'Delete:Role',
|
||||||
|
|
||||||
'View:Log',
|
'View:Log',
|
||||||
|
|
||||||
'View:Activity',
|
'View:Activity',
|
||||||
|
|||||||
@ -11,7 +11,21 @@ import {
|
|||||||
import { analysis, dashboard } from '@/routes';
|
import { analysis, dashboard } from '@/routes';
|
||||||
import category from '@/routes/category';
|
import category from '@/routes/category';
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import { BarChart3, Boxes, DollarSign, History, LayoutGrid, List, ScrollText, Settings, ShoppingBag, ShoppingCart, User, Wallet } from 'lucide-react';
|
import {
|
||||||
|
BarChart3,
|
||||||
|
Boxes,
|
||||||
|
DollarSign,
|
||||||
|
History,
|
||||||
|
LayoutGrid,
|
||||||
|
List,
|
||||||
|
ScrollText,
|
||||||
|
Settings,
|
||||||
|
ShieldCheck,
|
||||||
|
ShoppingBag,
|
||||||
|
ShoppingCart,
|
||||||
|
User,
|
||||||
|
Wallet,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
import expense from '@/routes/expense';
|
import expense from '@/routes/expense';
|
||||||
import order from '@/routes/order';
|
import order from '@/routes/order';
|
||||||
@ -21,8 +35,8 @@ import purchase from '@/routes/purchase';
|
|||||||
import system from '@/routes/system';
|
import system from '@/routes/system';
|
||||||
import user from '@/routes/user';
|
import user from '@/routes/user';
|
||||||
import type { NavItem } from '@/types';
|
import type { NavItem } from '@/types';
|
||||||
import { usePage } from '@inertiajs/react';
|
|
||||||
import type { Auth } from '@/types/auth';
|
import type { Auth } from '@/types/auth';
|
||||||
|
import { usePage } from '@inertiajs/react';
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
const mainNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
@ -97,6 +111,12 @@ const systemNavItems: NavItem[] = [
|
|||||||
icon: Settings,
|
icon: Settings,
|
||||||
permission: 'View:Setting',
|
permission: 'View:Setting',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Role & Izin',
|
||||||
|
href: system.role.index().url,
|
||||||
|
icon: ShieldCheck,
|
||||||
|
permission: 'View:Role',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Logs',
|
title: 'Logs',
|
||||||
href: system.logs.index().url,
|
href: system.logs.index().url,
|
||||||
@ -115,9 +135,9 @@ export function AppSidebar() {
|
|||||||
const { auth } = usePage<{ auth: Auth }>().props;
|
const { auth } = usePage<{ auth: Auth }>().props;
|
||||||
|
|
||||||
const filterNavItems = (items: NavItem[]) => {
|
const filterNavItems = (items: NavItem[]) => {
|
||||||
return items.filter(item => {
|
return items.filter((item) => {
|
||||||
if (!item.permission) return true;
|
if (!item.permission) return true;
|
||||||
return auth.permissions.includes(item.permission) || auth.roles.includes('Developer');
|
return auth.permissions.includes(item.permission);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -136,11 +156,33 @@ export function AppSidebar() {
|
|||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
{filterNavItems(mainNavItems).length > 0 && <NavMain items={filterNavItems(mainNavItems)} />}
|
{filterNavItems(mainNavItems).length > 0 && (
|
||||||
{filterNavItems(masterNavItems).length > 0 && <NavMain items={filterNavItems(masterNavItems)} label='Master' />}
|
<NavMain items={filterNavItems(mainNavItems)} />
|
||||||
{filterNavItems(manageNavItems).length > 0 && <NavMain items={filterNavItems(manageNavItems)} label='Kelola' />}
|
)}
|
||||||
{filterNavItems(financeNavItems).length > 0 && <NavMain items={filterNavItems(financeNavItems)} label='Keuangan' />}
|
{filterNavItems(masterNavItems).length > 0 && (
|
||||||
{filterNavItems(systemNavItems).length > 0 && <NavMain items={filterNavItems(systemNavItems)} label='Sistem' />}
|
<NavMain
|
||||||
|
items={filterNavItems(masterNavItems)}
|
||||||
|
label="Master"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filterNavItems(manageNavItems).length > 0 && (
|
||||||
|
<NavMain
|
||||||
|
items={filterNavItems(manageNavItems)}
|
||||||
|
label="Kelola"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filterNavItems(financeNavItems).length > 0 && (
|
||||||
|
<NavMain
|
||||||
|
items={filterNavItems(financeNavItems)}
|
||||||
|
label="Keuangan"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filterNavItems(systemNavItems).length > 0 && (
|
||||||
|
<NavMain
|
||||||
|
items={filterNavItems(systemNavItems)}
|
||||||
|
label="Sistem"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,20 +1,19 @@
|
|||||||
import type {
|
import type {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
SortingState,
|
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
VisibilityState} from "@tanstack/react-table";
|
SortingState,
|
||||||
|
VisibilityState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
import {
|
import {
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
getFilteredRowModel,
|
||||||
getPaginationRowModel,
|
getPaginationRowModel,
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
getFilteredRowModel
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Settings2, ChevronDown, ListFilter, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, X } from "lucide-react";
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import React from "react";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
@ -26,21 +25,32 @@ import {
|
|||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
ListFilter,
|
||||||
|
Settings2,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from "@/components/ui/empty"
|
} from '@/components/ui/empty';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from '@/components/ui/select';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -48,10 +58,10 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from '@/components/ui/table';
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from './ui/badge';
|
||||||
import { Button } from "./ui/button";
|
import { Button } from './ui/button';
|
||||||
import { Input } from "./ui/input";
|
import { Input } from './ui/input';
|
||||||
|
|
||||||
interface DataTableFilterOption {
|
interface DataTableFilterOption {
|
||||||
columnId: string;
|
columnId: string;
|
||||||
@ -63,7 +73,7 @@ export interface DataTableBulkAction<TData> {
|
|||||||
label: string;
|
label: string;
|
||||||
onClick: (rows: TData[]) => void;
|
onClick: (rows: TData[]) => void;
|
||||||
icon?: React.ElementType;
|
icon?: React.ElementType;
|
||||||
variant?: "default" | "destructive";
|
variant?: 'default' | 'destructive';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
@ -89,17 +99,19 @@ export function DataTable<TData, TValue>({
|
|||||||
onRowSelectionChange,
|
onRowSelectionChange,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] =
|
||||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
React.useState<ColumnFiltersState>([]);
|
||||||
const [globalFilter, setGlobalFilter] = React.useState("");
|
const [columnVisibility, setColumnVisibility] =
|
||||||
|
React.useState<VisibilityState>({});
|
||||||
|
const [globalFilter, setGlobalFilter] = React.useState('');
|
||||||
|
|
||||||
const finalColumns = React.useMemo(() => {
|
const finalColumns = React.useMemo(() => {
|
||||||
const computedColumns = [...columns];
|
const computedColumns = [...columns];
|
||||||
|
|
||||||
if (showNumbering) {
|
if (showNumbering) {
|
||||||
const numberingColumn: ColumnDef<TData, TValue> = {
|
const numberingColumn: ColumnDef<TData, TValue> = {
|
||||||
id: "numbering",
|
id: 'numbering',
|
||||||
header: "No.",
|
header: 'No.',
|
||||||
cell: ({ row }) => row.index + 1,
|
cell: ({ row }) => row.index + 1,
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
};
|
};
|
||||||
@ -108,14 +120,17 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
if (showSelection) {
|
if (showSelection) {
|
||||||
const selectColumn: ColumnDef<TData, TValue> = {
|
const selectColumn: ColumnDef<TData, TValue> = {
|
||||||
id: "select",
|
id: 'select',
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={
|
checked={
|
||||||
table.getIsAllPageRowsSelected() ||
|
table.getIsAllPageRowsSelected() ||
|
||||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
(table.getIsSomePageRowsSelected() &&
|
||||||
|
'indeterminate')
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
}
|
}
|
||||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
||||||
aria-label="Select all"
|
aria-label="Select all"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -160,31 +175,51 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||||
<div className="flex flex-1 flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
<div className="flex flex-1 flex-col items-stretch gap-2 sm:flex-row sm:items-center">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Cari..."
|
placeholder="Cari..."
|
||||||
value={globalFilter ?? ""}
|
value={globalFilter ?? ''}
|
||||||
onChange={(event) => setGlobalFilter(event.target.value)}
|
onChange={(event) =>
|
||||||
|
setGlobalFilter(event.target.value)
|
||||||
|
}
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{columnFilters.length > 0 && (
|
{columnFilters.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{columnFilters.map((filter) => {
|
{columnFilters.map((filter) => {
|
||||||
const filterDef = filters?.find((f) => f.columnId === filter.id);
|
const filterDef = filters?.find(
|
||||||
const option = filterDef?.options.find((o) => String(o.value) === String(filter.value));
|
(f) => f.columnId === filter.id,
|
||||||
const label = option ? option.label : String(filter.value);
|
);
|
||||||
|
const option = filterDef?.options.find(
|
||||||
|
(o) =>
|
||||||
|
String(o.value) ===
|
||||||
|
String(filter.value),
|
||||||
|
);
|
||||||
|
const label = option
|
||||||
|
? option.label
|
||||||
|
: String(filter.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge key={filter.id} variant="secondary" className="h-8 gap-1 pr-1 font-normal text-nowrap">
|
<Badge
|
||||||
<span className="text-muted-foreground mr-1">{filterDef?.title || filter.id}:</span>
|
key={filter.id}
|
||||||
|
variant="secondary"
|
||||||
|
className="h-8 gap-1 pr-1 font-normal text-nowrap"
|
||||||
|
>
|
||||||
|
<span className="mr-1 text-muted-foreground">
|
||||||
|
{filterDef?.title || filter.id}:
|
||||||
|
</span>
|
||||||
{label}
|
{label}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-4 w-4 hover:bg-transparent"
|
className="h-4 w-4 hover:bg-transparent"
|
||||||
onClick={() => table.getColumn(filter.id)?.setFilterValue(undefined)}
|
onClick={() =>
|
||||||
|
table
|
||||||
|
.getColumn(filter.id)
|
||||||
|
?.setFilterValue(undefined)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -196,7 +231,7 @@ export function DataTable<TData, TValue>({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setColumnFilters([]);
|
setColumnFilters([]);
|
||||||
setGlobalFilter("");
|
setGlobalFilter('');
|
||||||
}}
|
}}
|
||||||
className="h-8 px-2 text-xs"
|
className="h-8 px-2 text-xs"
|
||||||
>
|
>
|
||||||
@ -207,11 +242,14 @@ export function DataTable<TData, TValue>({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-auto">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{selectedRows.length > 0 && bulkActions && (
|
{selectedRows.length > 0 && bulkActions && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="secondary" className="gap-2 animate-in fade-in slide-in-from-left-2 transition-all">
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="animate-in gap-2 transition-all fade-in slide-in-from-left-2"
|
||||||
|
>
|
||||||
Aksi Massal ({selectedRows.length})
|
Aksi Massal ({selectedRows.length})
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -220,10 +258,22 @@ export function DataTable<TData, TValue>({
|
|||||||
{bulkActions.map((action, i) => (
|
{bulkActions.map((action, i) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => action.onClick(selectedRows.map((r) => r.original))}
|
onClick={() =>
|
||||||
className={action.variant === "destructive" ? "text-red-600 focus:text-red-600" : ""}
|
action.onClick(
|
||||||
|
selectedRows.map(
|
||||||
|
(r) => r.original,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
action.variant === 'destructive'
|
||||||
|
? 'text-red-600 focus:text-red-600'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{action.icon && <action.icon className="mr-2 h-4 w-4" />}
|
{action.icon && (
|
||||||
|
<action.icon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
{action.label}
|
{action.label}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
@ -240,10 +290,14 @@ export function DataTable<TData, TValue>({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuLabel>Filter Berdasarkan</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
Filter Berdasarkan
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{filters.map((filter) => {
|
{filters.map((filter) => {
|
||||||
const column = table.getColumn(filter.columnId);
|
const column = table.getColumn(
|
||||||
|
filter.columnId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!column) {
|
if (!column) {
|
||||||
return null;
|
return null;
|
||||||
@ -256,29 +310,51 @@ return null;
|
|||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuSubContent>
|
<DropdownMenuSubContent>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) =>
|
||||||
onClick={() => column.setFilterValue(undefined)}
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
column.setFilterValue(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Semua
|
Semua
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{filter.options.map((option) => (
|
{filter.options.map(
|
||||||
|
(option) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (option.value === "true") {
|
if (
|
||||||
column.setFilterValue(true);
|
option.value ===
|
||||||
} else if (option.value === "false") {
|
'true'
|
||||||
column.setFilterValue(false);
|
) {
|
||||||
|
column.setFilterValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
option.value ===
|
||||||
|
'false'
|
||||||
|
) {
|
||||||
|
column.setFilterValue(
|
||||||
|
false,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
column.setFilterValue(option.value);
|
column.setFilterValue(
|
||||||
|
option.value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</DropdownMenuSubContent>
|
</DropdownMenuSubContent>
|
||||||
</DropdownMenuSub>
|
</DropdownMenuSub>
|
||||||
);
|
);
|
||||||
@ -295,7 +371,9 @@ column.setFilterValue(option.value);
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuLabel>Tampilan Kolom</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
Tampilan Kolom
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{table
|
{table
|
||||||
.getAllColumns()
|
.getAllColumns()
|
||||||
@ -306,13 +384,20 @@ column.setFilterValue(option.value);
|
|||||||
key={column.id}
|
key={column.id}
|
||||||
className="capitalize"
|
className="capitalize"
|
||||||
checked={column.getIsVisible()}
|
checked={column.getIsVisible()}
|
||||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
onCheckedChange={(value) =>
|
||||||
|
column.toggleVisibility(!!value)
|
||||||
|
}
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
{(column.columnDef.meta as any)?.title ||
|
{(column.columnDef.meta as any)
|
||||||
(typeof column.columnDef.header === "string"
|
?.title ||
|
||||||
|
(typeof column.columnDef
|
||||||
|
.header === 'string'
|
||||||
? column.columnDef.header
|
? column.columnDef.header
|
||||||
: String(column.id).replace(/_/g, " "))}
|
: String(column.id).replace(
|
||||||
|
/_/g,
|
||||||
|
' ',
|
||||||
|
))}
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -331,14 +416,19 @@ column.setFilterValue(option.value);
|
|||||||
<TableHead
|
<TableHead
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className={
|
className={
|
||||||
header.column.id === "select" || header.column.id === "numbering"
|
header.column.id === 'select' ||
|
||||||
? "w-10 text-center"
|
header.column.id === 'numbering'
|
||||||
: ""
|
? 'w-10 text-center'
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
: flexRender(
|
||||||
|
header.column.columnDef
|
||||||
|
.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -348,24 +438,36 @@ column.setFilterValue(option.value);
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows?.length ? (
|
{table.getRowModel().rows?.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={
|
||||||
|
row.getIsSelected() && 'selected'
|
||||||
|
}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
className={
|
className={
|
||||||
cell.column.id === "select" || cell.column.id === "numbering"
|
cell.column.id === 'select' ||
|
||||||
? "w-10 text-center"
|
cell.column.id === 'numbering'
|
||||||
: ""
|
? 'w-10 text-center'
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={finalColumns.length} className="h-24 text-center">
|
<TableCell
|
||||||
|
colSpan={finalColumns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
<Empty>
|
<Empty>
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
<EmptyTitle>Ooops...</EmptyTitle>
|
<EmptyTitle>Ooops...</EmptyTitle>
|
||||||
@ -381,26 +483,33 @@ column.setFilterValue(option.value);
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 py-4">
|
<div className="flex flex-col items-center justify-between gap-4 py-4 sm:flex-row">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{table.getFilteredSelectedRowModel().rows.length} dari{" "}
|
{table.getFilteredSelectedRowModel().rows.length} dari{' '}
|
||||||
{table.getFilteredRowModel().rows.length} baris dipilih.
|
{table.getFilteredRowModel().rows.length} baris dipilih.
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row items-center gap-4 sm:gap-6 lg:gap-8">
|
<div className="flex flex-col items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-sm font-medium">Baris per halaman</p>
|
<p className="text-sm font-medium">Baris per halaman</p>
|
||||||
<Select
|
<Select
|
||||||
value={`${table.getState().pagination.pageSize}`}
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
table.setPageSize(Number(value))
|
table.setPageSize(Number(value));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-[70px]">
|
<SelectTrigger className="h-8 w-[70px]">
|
||||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
table.getState().pagination.pageSize
|
||||||
|
}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent side="top">
|
<SelectContent side="top">
|
||||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
<SelectItem
|
||||||
|
key={pageSize}
|
||||||
|
value={`${pageSize}`}
|
||||||
|
>
|
||||||
{pageSize}
|
{pageSize}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@ -410,12 +519,22 @@ column.setFilterValue(option.value);
|
|||||||
<div className="flex items-center justify-center text-sm font-medium">
|
<div className="flex items-center justify-center text-sm font-medium">
|
||||||
{table.getFilteredRowModel().rows.length > 0 ? (
|
{table.getFilteredRowModel().rows.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
Menampilkan {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} sampai{" "}
|
Menampilkan{' '}
|
||||||
{Math.min((table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length)}{" "}
|
{table.getState().pagination.pageIndex *
|
||||||
dari {table.getFilteredRowModel().rows.length} data
|
table.getState().pagination.pageSize +
|
||||||
|
1}{' '}
|
||||||
|
sampai{' '}
|
||||||
|
{Math.min(
|
||||||
|
(table.getState().pagination.pageIndex +
|
||||||
|
1) *
|
||||||
|
table.getState().pagination.pageSize,
|
||||||
|
table.getFilteredRowModel().rows.length,
|
||||||
|
)}{' '}
|
||||||
|
dari {table.getFilteredRowModel().rows.length}{' '}
|
||||||
|
data
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Tidak ada data"
|
'Tidak ada data'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@ -449,7 +568,9 @@ column.setFilterValue(option.value);
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
onClick={() =>
|
||||||
|
table.setPageIndex(table.getPageCount() - 1)
|
||||||
|
}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Halaman terakhir</span>
|
<span className="sr-only">Halaman terakhir</span>
|
||||||
|
|||||||
260
resources/js/pages/admin/system/role/create.tsx
Normal file
260
resources/js/pages/admin/system/role/create.tsx
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Field, FieldError } from '@/components/ui/field';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import roleRoutes from '@/routes/system/role';
|
||||||
|
import type { Permission } from '@/types';
|
||||||
|
import { Head, Link, useForm } from '@inertiajs/react';
|
||||||
|
import { ArrowLeft, Loader, Save } from 'lucide-react';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function RoleCreate({
|
||||||
|
permissions,
|
||||||
|
}: {
|
||||||
|
permissions: Permission[];
|
||||||
|
}) {
|
||||||
|
const { data, setData, post, processing, errors } = useForm<{
|
||||||
|
name: string;
|
||||||
|
guard_name: string;
|
||||||
|
permissions: number[];
|
||||||
|
}>({
|
||||||
|
name: '',
|
||||||
|
guard_name: 'web',
|
||||||
|
permissions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
post(roleRoutes.store().url, {
|
||||||
|
onSuccess: (response: any) => {
|
||||||
|
toast.success(response.props.flash.success);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupedPermissions = useMemo(() => {
|
||||||
|
const groups: Record<string, Permission[]> = {};
|
||||||
|
permissions.forEach((permission) => {
|
||||||
|
const parts = permission.name.split(':');
|
||||||
|
const groupName = parts.length > 1 ? parts[1] : 'Lainnya';
|
||||||
|
if (!groups[groupName]) {
|
||||||
|
groups[groupName] = [];
|
||||||
|
}
|
||||||
|
groups[groupName].push(permission);
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}, [permissions]);
|
||||||
|
|
||||||
|
const handleCheckboxChange = (id: number, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setData('permissions', [...data.permissions, id]);
|
||||||
|
} else {
|
||||||
|
setData(
|
||||||
|
'permissions',
|
||||||
|
data.permissions.filter((pId) => pId !== id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (groupName: string) => {
|
||||||
|
const groupPermissionIds = groupedPermissions[groupName].map(
|
||||||
|
(p) => p.id,
|
||||||
|
);
|
||||||
|
const allInGroupSelected = groupPermissionIds.every((id) =>
|
||||||
|
data.permissions.includes(id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allInGroupSelected) {
|
||||||
|
setData(
|
||||||
|
'permissions',
|
||||||
|
data.permissions.filter(
|
||||||
|
(id) => !groupPermissionIds.includes(id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const otherPermissions = data.permissions.filter(
|
||||||
|
(id) => !groupPermissionIds.includes(id),
|
||||||
|
);
|
||||||
|
setData('permissions', [
|
||||||
|
...otherPermissions,
|
||||||
|
...groupPermissionIds,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
setData(
|
||||||
|
'permissions',
|
||||||
|
permissions.map((p) => p.id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deselectAll = () => {
|
||||||
|
setData('permissions', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
<Head title="Tambah Role" />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
Tambah Role
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<Link href={roleRoutes.index().url}>
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
<ArrowLeft className="size-4" />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
|
<Card className="overflow-hidden border-none bg-card/50 shadow-lg backdrop-blur-sm">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="border-b bg-muted/30 p-6">
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<Field>
|
||||||
|
<Label htmlFor="name" required>
|
||||||
|
Nama
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={data.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setData('name', e.target.value)
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Contoh: Leader"
|
||||||
|
/>
|
||||||
|
<FieldError
|
||||||
|
error={errors.name}
|
||||||
|
label="Nama"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Label htmlFor="guard_name" required>
|
||||||
|
Guard
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="guard_name"
|
||||||
|
name="guard_name"
|
||||||
|
value={data.guard_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setData(
|
||||||
|
'guard_name',
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<FieldError
|
||||||
|
error={errors.guard_name}
|
||||||
|
label="Guard"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-8 p-6">
|
||||||
|
{Object.entries(groupedPermissions).map(
|
||||||
|
([groupName, groupPermissions]) => {
|
||||||
|
const allInGroupSelected =
|
||||||
|
groupPermissions.every((p) =>
|
||||||
|
data.permissions.includes(p.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={groupName}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
|
<h3 className="text-xs font-bold tracking-widest text-muted-foreground uppercase">
|
||||||
|
{groupName}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 gap-1.5"
|
||||||
|
onClick={() =>
|
||||||
|
toggleGroup(groupName)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{allInGroupSelected
|
||||||
|
? 'Lepas Semua'
|
||||||
|
: 'Pilih Semua'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{groupPermissions.map(
|
||||||
|
(permission) => (
|
||||||
|
<div
|
||||||
|
key={permission.id}
|
||||||
|
className="group flex items-center space-x-3"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`permission-${permission.id}`}
|
||||||
|
checked={data.permissions.includes(
|
||||||
|
permission.id,
|
||||||
|
)}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked,
|
||||||
|
) =>
|
||||||
|
handleCheckboxChange(
|
||||||
|
permission.id,
|
||||||
|
checked as boolean,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="transition-transform active:scale-90"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`permission-${permission.id}`}
|
||||||
|
className="cursor-pointer text-sm leading-none font-medium transition-colors group-hover:text-primary"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
permission.name.split(
|
||||||
|
':',
|
||||||
|
)[0]
|
||||||
|
}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" disabled={processing}>
|
||||||
|
{processing ? (
|
||||||
|
<Loader className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="size-4" />
|
||||||
|
)}
|
||||||
|
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleCreate.layout = {
|
||||||
|
breadcrumbs: [{ title: 'Sistem' }],
|
||||||
|
};
|
||||||
261
resources/js/pages/admin/system/role/edit.tsx
Normal file
261
resources/js/pages/admin/system/role/edit.tsx
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Field, FieldError } from '@/components/ui/field';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import roleRoutes from '@/routes/system/role';
|
||||||
|
import type { Permission, Role } from '@/types';
|
||||||
|
import { Head, Link, useForm } from '@inertiajs/react';
|
||||||
|
import { ArrowLeft, Loader, Save } from 'lucide-react';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function RoleEdit({
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
}: {
|
||||||
|
role: Role;
|
||||||
|
permissions: Permission[];
|
||||||
|
}) {
|
||||||
|
const { data, setData, put, processing, errors } = useForm<{
|
||||||
|
name: string;
|
||||||
|
guard_name: string;
|
||||||
|
permissions: number[];
|
||||||
|
}>({
|
||||||
|
name: role.name,
|
||||||
|
guard_name: role.guard_name,
|
||||||
|
permissions: role.permissions?.map((p: Permission) => p.id) || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
put(roleRoutes.update(role.id).url, {
|
||||||
|
onSuccess: (response: any) => {
|
||||||
|
toast.success(response.props.flash.success);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupedPermissions = useMemo(() => {
|
||||||
|
const groups: Record<string, Permission[]> = {};
|
||||||
|
permissions.forEach((permission) => {
|
||||||
|
const parts = permission.name.split(':');
|
||||||
|
const groupName = parts.length > 1 ? parts[1] : 'Lainnya';
|
||||||
|
if (!groups[groupName]) {
|
||||||
|
groups[groupName] = [];
|
||||||
|
}
|
||||||
|
groups[groupName].push(permission);
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}, [permissions]);
|
||||||
|
|
||||||
|
const handleCheckboxChange = (id: number, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setData('permissions', [...data.permissions, id]);
|
||||||
|
} else {
|
||||||
|
setData(
|
||||||
|
'permissions',
|
||||||
|
data.permissions.filter((pId) => pId !== id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (groupName: string) => {
|
||||||
|
const groupPermissionIds = groupedPermissions[groupName].map(
|
||||||
|
(p) => p.id,
|
||||||
|
);
|
||||||
|
const allInGroupSelected = groupPermissionIds.every((id) =>
|
||||||
|
data.permissions.includes(id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allInGroupSelected) {
|
||||||
|
setData(
|
||||||
|
'permissions',
|
||||||
|
data.permissions.filter(
|
||||||
|
(id) => !groupPermissionIds.includes(id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const otherPermissions = data.permissions.filter(
|
||||||
|
(id) => !groupPermissionIds.includes(id),
|
||||||
|
);
|
||||||
|
setData('permissions', [
|
||||||
|
...otherPermissions,
|
||||||
|
...groupPermissionIds,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
setData(
|
||||||
|
'permissions',
|
||||||
|
permissions.map((p) => p.id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deselectAll = () => {
|
||||||
|
setData('permissions', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
<Head title="Ubah Role" />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
Ubah Role
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<Link href={roleRoutes.index().url}>
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
<ArrowLeft className="size-4" />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
|
<Card className="overflow-hidden border-none bg-card/50 shadow-lg backdrop-blur-sm">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="border-b bg-muted/30 p-6">
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<Field>
|
||||||
|
<Label htmlFor="name" required>
|
||||||
|
Nama
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={data.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setData('name', e.target.value)
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<FieldError
|
||||||
|
error={errors.name}
|
||||||
|
label="Nama"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Label htmlFor="guard_name" required>
|
||||||
|
Guard
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="guard_name"
|
||||||
|
name="guard_name"
|
||||||
|
value={data.guard_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setData(
|
||||||
|
'guard_name',
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<FieldError
|
||||||
|
error={errors.guard_name}
|
||||||
|
label="Guard"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-8 p-6">
|
||||||
|
{Object.entries(groupedPermissions).map(
|
||||||
|
([groupName, groupPermissions]) => {
|
||||||
|
const allInGroupSelected =
|
||||||
|
groupPermissions.every((p) =>
|
||||||
|
data.permissions.includes(p.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={groupName}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
|
<h3 className="text-xs font-bold tracking-widest text-muted-foreground uppercase">
|
||||||
|
{groupName}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 gap-1.5"
|
||||||
|
onClick={() =>
|
||||||
|
toggleGroup(groupName)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{allInGroupSelected
|
||||||
|
? 'Lepas Semua'
|
||||||
|
: 'Pilih Semua'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{groupPermissions.map(
|
||||||
|
(permission) => (
|
||||||
|
<div
|
||||||
|
key={permission.id}
|
||||||
|
className="group flex items-center space-x-3"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`permission-${permission.id}`}
|
||||||
|
checked={data.permissions.includes(
|
||||||
|
permission.id,
|
||||||
|
)}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked,
|
||||||
|
) =>
|
||||||
|
handleCheckboxChange(
|
||||||
|
permission.id,
|
||||||
|
checked as boolean,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="transition-transform active:scale-90"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`permission-${permission.id}`}
|
||||||
|
className="cursor-pointer text-sm leading-none font-medium transition-colors group-hover:text-primary"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
permission.name.split(
|
||||||
|
':',
|
||||||
|
)[0]
|
||||||
|
}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" disabled={processing}>
|
||||||
|
{processing ? (
|
||||||
|
<Loader className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="size-4" />
|
||||||
|
)}
|
||||||
|
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleEdit.layout = {
|
||||||
|
breadcrumbs: [{ title: 'Sistem' }],
|
||||||
|
};
|
||||||
60
resources/js/pages/admin/system/role/hooks/use-role-index.ts
Normal file
60
resources/js/pages/admin/system/role/hooks/use-role-index.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { router } from '@inertiajs/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import roleRoutes from '@/routes/system/role';
|
||||||
|
import type { Role } from '@/types';
|
||||||
|
|
||||||
|
export function useRoleIndex() {
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||||
|
const [roleToDelete, setRoleToDelete] = useState<Role | null>(null);
|
||||||
|
const [rowsToDelete, setRowsToDelete] = useState<any[]>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
|
|
||||||
|
const onDelete = (role: Role) => {
|
||||||
|
setRoleToDelete(role);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (roleToDelete) {
|
||||||
|
router.delete(roleRoutes.destroy(roleToDelete.id).url, {
|
||||||
|
onSuccess: (response: any) => {
|
||||||
|
toast.success(response.props.flash.success);
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
setRoleToDelete(null);
|
||||||
|
setRowSelection({});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmBulkDelete = () => {
|
||||||
|
router.post(roleRoutes.bulkDestroy().url, {
|
||||||
|
ids: rowsToDelete.map((row: any) => row.id),
|
||||||
|
_method: 'DELETE'
|
||||||
|
}, {
|
||||||
|
onSuccess: (response: any) => {
|
||||||
|
toast.success(response.props.flash.success);
|
||||||
|
setIsBulkDeleteDialogOpen(false);
|
||||||
|
setRowsToDelete([]);
|
||||||
|
setRowSelection({});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDeleteDialogOpen,
|
||||||
|
isBulkDeleteDialogOpen,
|
||||||
|
roleToDelete,
|
||||||
|
rowsToDelete,
|
||||||
|
rowSelection,
|
||||||
|
setRowSelection,
|
||||||
|
setRowsToDelete,
|
||||||
|
setIsDeleteDialogOpen,
|
||||||
|
setIsBulkDeleteDialogOpen,
|
||||||
|
onDelete,
|
||||||
|
confirmDelete,
|
||||||
|
confirmBulkDelete,
|
||||||
|
};
|
||||||
|
}
|
||||||
114
resources/js/pages/admin/system/role/index.tsx
Normal file
114
resources/js/pages/admin/system/role/index.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { Head, Link } from '@inertiajs/react';
|
||||||
|
import { Trash2, Plus } from 'lucide-react';
|
||||||
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import roleRoutes from '@/routes/system/role';
|
||||||
|
import type { Role } from '@/types';
|
||||||
|
import { useRoleIndex } from './hooks/use-role-index';
|
||||||
|
import { getColumns } from './partials/columns';
|
||||||
|
import { usePermission } from '@/hooks/use-permission';
|
||||||
|
|
||||||
|
export default function RoleIndex({ roles }: { roles: Role[] }) {
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
const {
|
||||||
|
isDeleteDialogOpen,
|
||||||
|
isBulkDeleteDialogOpen,
|
||||||
|
roleToDelete,
|
||||||
|
rowsToDelete,
|
||||||
|
rowSelection,
|
||||||
|
setRowSelection,
|
||||||
|
setRowsToDelete,
|
||||||
|
setIsDeleteDialogOpen,
|
||||||
|
setIsBulkDeleteDialogOpen,
|
||||||
|
onDelete,
|
||||||
|
confirmDelete,
|
||||||
|
confirmBulkDelete,
|
||||||
|
} = useRoleIndex();
|
||||||
|
|
||||||
|
const columns = getColumns({ onDelete }).filter(
|
||||||
|
(col) => col.id !== 'actions' || hasPermission('Edit:Role') || hasPermission('Delete:Role')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 p-6">
|
||||||
|
<Head title="Role & Izin Akses" />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Role & Izin Akses</h1>
|
||||||
|
</div>
|
||||||
|
{hasPermission('Create:Role') && (
|
||||||
|
<Link href={roleRoutes.create().url}>
|
||||||
|
<Button className="gap-2">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
Tambah
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={roles}
|
||||||
|
showSelection={hasPermission('DeleteAny:Role')}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
onRowSelectionChange={setRowSelection}
|
||||||
|
bulkActions={
|
||||||
|
hasPermission('DeleteAny:Role')
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Hapus Terpilih',
|
||||||
|
onClick: (rows) => {
|
||||||
|
setRowsToDelete(rows);
|
||||||
|
setIsBulkDeleteDialogOpen(true);
|
||||||
|
},
|
||||||
|
icon: Trash2,
|
||||||
|
variant: 'destructive',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Single Delete Confirmation */}
|
||||||
|
<DeleteConfirmation
|
||||||
|
isOpen={isDeleteDialogOpen}
|
||||||
|
onOpenChange={setIsDeleteDialogOpen}
|
||||||
|
title="Hapus role?"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
Tindakan ini tidak dapat dibatalkan. Role <strong>{roleToDelete?.name}</strong> akan dihapus secara permanen.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onDelete={confirmDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Bulk Delete Confirmation */}
|
||||||
|
<DeleteConfirmation
|
||||||
|
isOpen={isBulkDeleteDialogOpen}
|
||||||
|
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||||
|
title={`Hapus ${rowsToDelete.length} role?`}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
Tindakan ini tidak dapat dibatalkan. <strong>{rowsToDelete.length}</strong> item yang terpilih akan dihapus secara permanen.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onDelete={confirmBulkDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleIndex.layout = {
|
||||||
|
breadcrumbs: [
|
||||||
|
{
|
||||||
|
title: 'Sistem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
114
resources/js/pages/admin/system/role/partials/columns.tsx
Normal file
114
resources/js/pages/admin/system/role/partials/columns.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { DataTableColumnHeader } from '@/components/data-table-column-header';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { usePermission } from '@/hooks/use-permission';
|
||||||
|
import roleRoutes from '@/routes/system/role';
|
||||||
|
import type { Role } from '@/types';
|
||||||
|
import { Link } from '@inertiajs/react';
|
||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ColumnProps {
|
||||||
|
onDelete: (role: Role) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getColumns = ({ onDelete }: ColumnProps): ColumnDef<Role>[] => [
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title="Nama" />
|
||||||
|
),
|
||||||
|
meta: { title: 'Nama' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'permissions',
|
||||||
|
header: 'Izin Akses',
|
||||||
|
meta: { title: 'Izin Akses' },
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const permissions = row.original.permissions || [];
|
||||||
|
|
||||||
|
if (permissions.length === 0) {
|
||||||
|
return (
|
||||||
|
<span className="text-xs text-muted-foreground italic">
|
||||||
|
Tidak ada izin akses
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex max-w-md flex-wrap gap-1">
|
||||||
|
{permissions.map((p) => (
|
||||||
|
<Badge
|
||||||
|
key={p.id}
|
||||||
|
variant="secondary"
|
||||||
|
className="px-1 py-0 text-[10px] font-normal"
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'guard_name',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title="Guard" />
|
||||||
|
),
|
||||||
|
meta: { title: 'Guard' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const role = row.original;
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{hasPermission('Edit:Role') && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link href={roleRoutes.edit(role.id).url}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-yellow-600 hover:bg-yellow-50 hover:text-yellow-700 dark:hover:bg-yellow-950/20"
|
||||||
|
>
|
||||||
|
<Pencil className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Ubah</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{hasPermission('Delete:Role') && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-red-600 hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/20"
|
||||||
|
onClick={() => onDelete(role)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Hapus</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
meta: { title: 'Aksi' },
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -10,3 +10,5 @@ export type * from './general-setting';
|
|||||||
export type * from './activity';
|
export type * from './activity';
|
||||||
export type * from './system-log';
|
export type * from './system-log';
|
||||||
export type * from './dashboard';
|
export type * from './dashboard';
|
||||||
|
export type * from './role';
|
||||||
|
export type * from './permission';
|
||||||
|
|||||||
7
resources/js/types/permission.ts
Normal file
7
resources/js/types/permission.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Permission {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
guard_name: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string | null;
|
||||||
|
}
|
||||||
10
resources/js/types/role.ts
Normal file
10
resources/js/types/role.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { Permission } from './permission';
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
guard_name: string;
|
||||||
|
permissions?: Permission[];
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string | null;
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
use App\Http\Controllers\Admin\System\ActivityLogController;
|
use App\Http\Controllers\Admin\System\ActivityLogController;
|
||||||
use App\Http\Controllers\Admin\System\GeneralSettingController;
|
use App\Http\Controllers\Admin\System\GeneralSettingController;
|
||||||
use App\Http\Controllers\Admin\System\LogController;
|
use App\Http\Controllers\Admin\System\LogController;
|
||||||
|
use App\Http\Controllers\Admin\System\RoleController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::middleware(['auth'])->group(function () {
|
Route::middleware(['auth'])->group(function () {
|
||||||
@ -10,6 +11,16 @@
|
|||||||
Route::get('logs', [LogController::class, 'index'])->name('system.logs.index')->middleware('can:View:Log');
|
Route::get('logs', [LogController::class, 'index'])->name('system.logs.index')->middleware('can:View:Log');
|
||||||
Route::get('activity-logs', [ActivityLogController::class, 'index'])->name('system.activity-logs.index')->middleware('can:View:Activity');
|
Route::get('activity-logs', [ActivityLogController::class, 'index'])->name('system.activity-logs.index')->middleware('can:View:Activity');
|
||||||
|
|
||||||
|
Route::prefix('role')->controller(RoleController::class)->group(function () {
|
||||||
|
Route::get('/', 'index')->name('system.role.index')->middleware('can:View:Role');
|
||||||
|
Route::get('/create', 'create')->name('system.role.create')->middleware('can:Create:Role');
|
||||||
|
Route::post('/', 'store')->name('system.role.store')->middleware('can:Create:Role');
|
||||||
|
Route::get('/{role}/edit', 'edit')->name('system.role.edit')->middleware('can:Edit:Role');
|
||||||
|
Route::put('/{role}', 'update')->name('system.role.update')->middleware('can:Edit:Role');
|
||||||
|
Route::delete('/{role}', 'destroy')->name('system.role.destroy')->middleware('can:Delete:Role');
|
||||||
|
Route::post('/bulk-destroy', 'bulkDestroy')->name('system.role.bulk-destroy')->middleware('can:DeleteAny:Role');
|
||||||
|
});
|
||||||
|
|
||||||
Route::controller(GeneralSettingController::class)->prefix('settings')->group(function () {
|
Route::controller(GeneralSettingController::class)->prefix('settings')->group(function () {
|
||||||
Route::get('/', 'index')->name('system.settings.index')->middleware('can:View:Setting');
|
Route::get('/', 'index')->name('system.settings.index')->middleware('can:View:Setting');
|
||||||
Route::post('/', 'update')->name('system.settings.update')->middleware('can:Edit:Setting');
|
Route::post('/', 'update')->name('system.settings.update')->middleware('can:Edit:Setting');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user