feat: implement Role management system with CRUD operations, including role creation, editing, and deletion, along with permission assignment

This commit is contained in:
Yoga Pangestu 2026-04-29 20:08:03 +07:00
parent 0fdd02e9e4
commit 96ad0de44c
14 changed files with 1263 additions and 100 deletions

View 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');
}
}

View 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')],
];
}
}

View File

@ -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',

View File

@ -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>
); );

View File

@ -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>

View 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' }],
};

View 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' }],
};

View 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,
};
}

View 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',
},
],
};

View 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' },
},
];

View File

@ -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';

View File

@ -0,0 +1,7 @@
export interface Permission {
id: number;
name: string;
guard_name: string;
created_at: string;
updated_at: string | null;
}

View 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;
}

View File

@ -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');