diff --git a/app/Http/Controllers/Admin/System/RoleController.php b/app/Http/Controllers/Admin/System/RoleController.php new file mode 100644 index 0000000..3eea6b5 --- /dev/null +++ b/app/Http/Controllers/Admin/System/RoleController.php @@ -0,0 +1,112 @@ + 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'); + } +} diff --git a/app/Http/Requests/Admin/System/RoleRequest.php b/app/Http/Requests/Admin/System/RoleRequest.php new file mode 100644 index 0000000..3f8a75c --- /dev/null +++ b/app/Http/Requests/Admin/System/RoleRequest.php @@ -0,0 +1,31 @@ +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')], + ]; + } +} diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 962b9ae..5985617 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -82,6 +82,12 @@ public function run(): void Permission::firstOrCreate(['name' => 'View: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 Permission::firstOrCreate(['name' => 'View:Log', 'guard_name' => 'web']); @@ -98,7 +104,14 @@ public function run(): void $developer->syncPermissions($allPermissions); // 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); // Assign permissions to Admin @@ -138,6 +151,11 @@ public function run(): void 'View:Setting', 'Edit:Setting', + 'View:Role', + 'Create:Role', + 'Edit:Role', + 'Delete:Role', + 'View:Log', 'View:Activity', diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index b258612..df9c99e 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -11,7 +11,21 @@ import { import { analysis, dashboard } from '@/routes'; import category from '@/routes/category'; 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 order from '@/routes/order'; @@ -21,8 +35,8 @@ import purchase from '@/routes/purchase'; import system from '@/routes/system'; import user from '@/routes/user'; import type { NavItem } from '@/types'; -import { usePage } from '@inertiajs/react'; import type { Auth } from '@/types/auth'; +import { usePage } from '@inertiajs/react'; const mainNavItems: NavItem[] = [ { @@ -97,6 +111,12 @@ const systemNavItems: NavItem[] = [ icon: Settings, permission: 'View:Setting', }, + { + title: 'Role & Izin', + href: system.role.index().url, + icon: ShieldCheck, + permission: 'View:Role', + }, { title: 'Logs', href: system.logs.index().url, @@ -112,12 +132,12 @@ const systemNavItems: NavItem[] = [ ]; export function AppSidebar() { - const { auth } = usePage<{auth: Auth}>().props; + const { auth } = usePage<{ auth: Auth }>().props; const filterNavItems = (items: NavItem[]) => { - return items.filter(item => { + return items.filter((item) => { 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() { - {filterNavItems(mainNavItems).length > 0 && } - {filterNavItems(masterNavItems).length > 0 && } - {filterNavItems(manageNavItems).length > 0 && } - {filterNavItems(financeNavItems).length > 0 && } - {filterNavItems(systemNavItems).length > 0 && } + {filterNavItems(mainNavItems).length > 0 && ( + + )} + {filterNavItems(masterNavItems).length > 0 && ( + + )} + {filterNavItems(manageNavItems).length > 0 && ( + + )} + {filterNavItems(financeNavItems).length > 0 && ( + + )} + {filterNavItems(systemNavItems).length > 0 && ( + + )} ); diff --git a/resources/js/components/data-table.tsx b/resources/js/components/data-table.tsx index 6e177af..bd84aba 100644 --- a/resources/js/components/data-table.tsx +++ b/resources/js/components/data-table.tsx @@ -1,20 +1,19 @@ import type { ColumnDef, - SortingState, ColumnFiltersState, - VisibilityState} from "@tanstack/react-table"; + SortingState, + VisibilityState, +} from '@tanstack/react-table'; import { flexRender, getCoreRowModel, - useReactTable, + getFilteredRowModel, getPaginationRowModel, getSortedRowModel, - getFilteredRowModel -} from "@tanstack/react-table"; + useReactTable, +} from '@tanstack/react-table'; -import { Settings2, ChevronDown, ListFilter, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, X } from "lucide-react"; -import React from "react"; -import { Checkbox } from "@/components/ui/checkbox"; +import { Checkbox } from '@/components/ui/checkbox'; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -26,21 +25,32 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger, 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 { Empty, EmptyDescription, EmptyHeader, EmptyTitle, -} from "@/components/ui/empty" +} from '@/components/ui/empty'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select" +} from '@/components/ui/select'; import { Table, TableBody, @@ -48,10 +58,10 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table"; -import { Badge } from "./ui/badge"; -import { Button } from "./ui/button"; -import { Input } from "./ui/input"; +} from '@/components/ui/table'; +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; interface DataTableFilterOption { columnId: string; @@ -63,7 +73,7 @@ export interface DataTableBulkAction { label: string; onClick: (rows: TData[]) => void; icon?: React.ElementType; - variant?: "default" | "destructive"; + variant?: 'default' | 'destructive'; } interface DataTableProps { @@ -89,17 +99,19 @@ export function DataTable({ onRowSelectionChange, }: DataTableProps) { const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState([]); - const [columnVisibility, setColumnVisibility] = React.useState({}); - const [globalFilter, setGlobalFilter] = React.useState(""); + const [columnFilters, setColumnFilters] = + React.useState([]); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [globalFilter, setGlobalFilter] = React.useState(''); const finalColumns = React.useMemo(() => { const computedColumns = [...columns]; if (showNumbering) { const numberingColumn: ColumnDef = { - id: "numbering", - header: "No.", + id: 'numbering', + header: 'No.', cell: ({ row }) => row.index + 1, enableHiding: false, }; @@ -108,14 +120,17 @@ export function DataTable({ if (showSelection) { const selectColumn: ColumnDef = { - id: "select", + id: 'select', header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value) } - onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" /> ), @@ -160,31 +175,51 @@ export function DataTable({ return (
-
-
+
+
setGlobalFilter(event.target.value)} + value={globalFilter ?? ''} + onChange={(event) => + setGlobalFilter(event.target.value) + } className="max-w-sm" /> {columnFilters.length > 0 && (
{columnFilters.map((filter) => { - const filterDef = filters?.find((f) => f.columnId === filter.id); - const option = filterDef?.options.find((o) => String(o.value) === String(filter.value)); - const label = option ? option.label : String(filter.value); + const filterDef = filters?.find( + (f) => f.columnId === filter.id, + ); + const option = filterDef?.options.find( + (o) => + String(o.value) === + String(filter.value), + ); + const label = option + ? option.label + : String(filter.value); return ( - - {filterDef?.title || filter.id}: + + + {filterDef?.title || filter.id}: + {label} @@ -196,7 +231,7 @@ export function DataTable({ size="sm" onClick={() => { setColumnFilters([]); - setGlobalFilter(""); + setGlobalFilter(''); }} className="h-8 px-2 text-xs" > @@ -207,11 +242,14 @@ export function DataTable({ )}
-
+
{selectedRows.length > 0 && bulkActions && ( - @@ -220,10 +258,22 @@ export function DataTable({ {bulkActions.map((action, i) => ( action.onClick(selectedRows.map((r) => r.original))} - className={action.variant === "destructive" ? "text-red-600 focus:text-red-600" : ""} + onClick={() => + action.onClick( + selectedRows.map( + (r) => r.original, + ), + ) + } + className={ + action.variant === 'destructive' + ? 'text-red-600 focus:text-red-600' + : '' + } > - {action.icon && } + {action.icon && ( + + )} {action.label} ))} @@ -240,14 +290,18 @@ export function DataTable({ - Filter Berdasarkan + + Filter Berdasarkan + {filters.map((filter) => { - const column = table.getColumn(filter.columnId); + const column = table.getColumn( + filter.columnId, + ); if (!column) { -return null; -} + return null; + } return ( @@ -256,29 +310,51 @@ return null; e.preventDefault()} - onClick={() => column.setFilterValue(undefined)} + onSelect={(e) => + e.preventDefault() + } + onClick={() => + column.setFilterValue( + undefined, + ) + } > Semua - {filter.options.map((option) => ( - e.preventDefault()} - onClick={() => { - if (option.value === "true") { -column.setFilterValue(true); -} else if (option.value === "false") { -column.setFilterValue(false); -} else { -column.setFilterValue(option.value); -} - }} - > - {option.label} - - ))} + {filter.options.map( + (option) => ( + + e.preventDefault() + } + onClick={() => { + if ( + option.value === + 'true' + ) { + column.setFilterValue( + true, + ); + } else if ( + option.value === + 'false' + ) { + column.setFilterValue( + false, + ); + } else { + column.setFilterValue( + option.value, + ); + } + }} + > + {option.label} + + ), + )} ); @@ -295,7 +371,9 @@ column.setFilterValue(option.value); - Tampilan Kolom + + Tampilan Kolom + {table .getAllColumns() @@ -306,13 +384,20 @@ column.setFilterValue(option.value); key={column.id} className="capitalize" checked={column.getIsVisible()} - onCheckedChange={(value) => column.toggleVisibility(!!value)} + onCheckedChange={(value) => + column.toggleVisibility(!!value) + } onSelect={(e) => e.preventDefault()} > - {(column.columnDef.meta as any)?.title || - (typeof column.columnDef.header === "string" + {(column.columnDef.meta as any) + ?.title || + (typeof column.columnDef + .header === 'string' ? column.columnDef.header - : String(column.id).replace(/_/g, " "))} + : String(column.id).replace( + /_/g, + ' ', + ))} ); })} @@ -331,14 +416,19 @@ column.setFilterValue(option.value); {header.isPlaceholder ? null - : flexRender(header.column.columnDef.header, header.getContext())} + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} ); })} @@ -348,24 +438,36 @@ column.setFilterValue(option.value); {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} ))} )) ) : ( - + Ooops... @@ -381,26 +483,33 @@ column.setFilterValue(option.value);
-
+
- {table.getFilteredSelectedRowModel().rows.length} dari{" "} + {table.getFilteredSelectedRowModel().rows.length} dari{' '} {table.getFilteredRowModel().rows.length} baris dipilih.
-
+

Baris per halaman

+ setData('name', e.target.value) + } + autoComplete="off" + placeholder="Contoh: Leader" + /> + + + + + + + setData( + 'guard_name', + e.target.value, + ) + } + autoComplete="off" + /> + + +
+
+ +
+ {Object.entries(groupedPermissions).map( + ([groupName, groupPermissions]) => { + const allInGroupSelected = + groupPermissions.every((p) => + data.permissions.includes(p.id), + ); + + return ( +
+
+

+ {groupName} +

+ +
+
+ {groupPermissions.map( + (permission) => ( +
+ + handleCheckboxChange( + permission.id, + checked as boolean, + ) + } + className="transition-transform active:scale-90" + /> + +
+ ), + )} +
+
+ ); + }, + )} +
+ + + +
+ +
+ +
+ ); +} + +RoleCreate.layout = { + breadcrumbs: [{ title: 'Sistem' }], +}; diff --git a/resources/js/pages/admin/system/role/edit.tsx b/resources/js/pages/admin/system/role/edit.tsx new file mode 100644 index 0000000..4f51445 --- /dev/null +++ b/resources/js/pages/admin/system/role/edit.tsx @@ -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 = {}; + 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 ( +
+ + +
+
+

+ Ubah Role +

+
+ + + +
+ +
+ + +
+
+ + + + setData('name', e.target.value) + } + autoComplete="off" + /> + + + + + + + setData( + 'guard_name', + e.target.value, + ) + } + autoComplete="off" + /> + + +
+
+ +
+ {Object.entries(groupedPermissions).map( + ([groupName, groupPermissions]) => { + const allInGroupSelected = + groupPermissions.every((p) => + data.permissions.includes(p.id), + ); + + return ( +
+
+

+ {groupName} +

+ +
+
+ {groupPermissions.map( + (permission) => ( +
+ + handleCheckboxChange( + permission.id, + checked as boolean, + ) + } + className="transition-transform active:scale-90" + /> + +
+ ), + )} +
+
+ ); + }, + )} +
+
+
+ +
+ +
+
+
+ ); +} + +RoleEdit.layout = { + breadcrumbs: [{ title: 'Sistem' }], +}; diff --git a/resources/js/pages/admin/system/role/hooks/use-role-index.ts b/resources/js/pages/admin/system/role/hooks/use-role-index.ts new file mode 100644 index 0000000..44fcd66 --- /dev/null +++ b/resources/js/pages/admin/system/role/hooks/use-role-index.ts @@ -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(null); + const [rowsToDelete, setRowsToDelete] = useState([]); + 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, + }; +} diff --git a/resources/js/pages/admin/system/role/index.tsx b/resources/js/pages/admin/system/role/index.tsx new file mode 100644 index 0000000..3693739 --- /dev/null +++ b/resources/js/pages/admin/system/role/index.tsx @@ -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 ( +
+ + +
+
+

Role & Izin Akses

+
+ {hasPermission('Create:Role') && ( + + + + )} +
+ + + + { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } + /> + + + + {/* Single Delete Confirmation */} + + Tindakan ini tidak dapat dibatalkan. Role {roleToDelete?.name} akan dihapus secara permanen. + + } + onDelete={confirmDelete} + /> + + {/* Bulk Delete Confirmation */} + + Tindakan ini tidak dapat dibatalkan. {rowsToDelete.length} item yang terpilih akan dihapus secara permanen. + + } + onDelete={confirmBulkDelete} + /> +
+ ); +} + +RoleIndex.layout = { + breadcrumbs: [ + { + title: 'Sistem', + }, + ], +}; diff --git a/resources/js/pages/admin/system/role/partials/columns.tsx b/resources/js/pages/admin/system/role/partials/columns.tsx new file mode 100644 index 0000000..db36b73 --- /dev/null +++ b/resources/js/pages/admin/system/role/partials/columns.tsx @@ -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[] => [ + { + accessorKey: 'name', + header: ({ column }) => ( + + ), + meta: { title: 'Nama' }, + }, + { + accessorKey: 'permissions', + header: 'Izin Akses', + meta: { title: 'Izin Akses' }, + cell: ({ row }) => { + const permissions = row.original.permissions || []; + + if (permissions.length === 0) { + return ( + + Tidak ada izin akses + + ); + } + + return ( +
+ {permissions.map((p) => ( + + {p.name} + + ))} +
+ ); + }, + }, + { + accessorKey: 'guard_name', + header: ({ column }) => ( + + ), + meta: { title: 'Guard' }, + }, + { + id: 'actions', + header: 'Aksi', + cell: ({ row }) => { + const role = row.original; + const { hasPermission } = usePermission(); + + return ( +
+ {hasPermission('Edit:Role') && ( + + + + + + + +

Ubah

+
+
+ )} + {hasPermission('Delete:Role') && ( + + + + + +

Hapus

+
+
+ )} +
+ ); + }, + meta: { title: 'Aksi' }, + }, +]; diff --git a/resources/js/types/index.ts b/resources/js/types/index.ts index 6f249fb..5457605 100644 --- a/resources/js/types/index.ts +++ b/resources/js/types/index.ts @@ -10,3 +10,5 @@ export type * from './general-setting'; export type * from './activity'; export type * from './system-log'; export type * from './dashboard'; +export type * from './role'; +export type * from './permission'; diff --git a/resources/js/types/permission.ts b/resources/js/types/permission.ts new file mode 100644 index 0000000..937d09d --- /dev/null +++ b/resources/js/types/permission.ts @@ -0,0 +1,7 @@ +export interface Permission { + id: number; + name: string; + guard_name: string; + created_at: string; + updated_at: string | null; +} diff --git a/resources/js/types/role.ts b/resources/js/types/role.ts new file mode 100644 index 0000000..c6fed2f --- /dev/null +++ b/resources/js/types/role.ts @@ -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; +} diff --git a/routes/system.php b/routes/system.php index 93fe8a5..2d7238b 100644 --- a/routes/system.php +++ b/routes/system.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Admin\System\ActivityLogController; use App\Http\Controllers\Admin\System\GeneralSettingController; use App\Http\Controllers\Admin\System\LogController; +use App\Http\Controllers\Admin\System\RoleController; use Illuminate\Support\Facades\Route; 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('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::get('/', 'index')->name('system.settings.index')->middleware('can:View:Setting'); Route::post('/', 'update')->name('system.settings.update')->middleware('can:Edit:Setting');