diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 9ffc373..77ac6a3 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -39,7 +39,9 @@ public function share(Request $request): array ...parent::share($request), 'name' => config('app.name'), 'auth' => [ - 'user' => $request->user()?->load('profile'), + 'user' => $request->user() ? $request->user()->load('profile') : null, + 'roles' => $request->user() ? $request->user()->getRoleNames() : [], + 'permissions' => $request->user() ? $request->user()->getAllPermissions()->pluck('name') : [], ], 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'flash' => [ diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 7f84a1d..b258612 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -21,17 +21,21 @@ 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'; const mainNavItems: NavItem[] = [ { title: 'Dasbor', href: dashboard().url, icon: LayoutGrid, + permission: 'View:Dashboard', }, { title: 'Analisa', href: analysis().url, icon: BarChart3, + permission: 'View:Analysis', }, ]; @@ -40,16 +44,19 @@ const masterNavItems: NavItem[] = [ title: 'Pegawai', href: user.index().url, icon: User, + permission: 'View:User', }, { title: 'Kategori', href: category.index().url, icon: List, + permission: 'View:Category', }, { title: 'Produk', href: product.index().url, icon: Boxes, + permission: 'View:Product', }, ]; @@ -58,11 +65,13 @@ const manageNavItems: NavItem[] = [ title: 'Pesanan', href: order.index().url, icon: ShoppingBag, + permission: 'View:Order', }, { title: 'Belanja', href: purchase.index().url, icon: ShoppingCart, + permission: 'View:Purchase', }, ]; @@ -71,11 +80,13 @@ const financeNavItems: NavItem[] = [ title: 'Pengeluaran', href: expense.index().url, icon: Wallet, + permission: 'View:Expense', }, { title: 'Penggajian', href: payroll.index().url, icon: DollarSign, + permission: 'View:Payroll', }, ]; @@ -84,20 +95,32 @@ const systemNavItems: NavItem[] = [ title: 'Pengaturan', href: system.settings.index().url, icon: Settings, + permission: 'View:Setting', }, { title: 'Logs', href: system.logs.index().url, icon: ScrollText, + permission: 'View:Log', }, { title: 'Riwayat', href: system.activityLogs.index().url, icon: History, + permission: 'View:Activity', }, ]; export function AppSidebar() { + const { auth } = usePage<{auth: Auth}>().props; + + const filterNavItems = (items: NavItem[]) => { + return items.filter(item => { + if (!item.permission) return true; + return auth.permissions.includes(item.permission) || auth.roles.includes('Developer'); + }); + }; + return ( @@ -113,11 +136,11 @@ export function AppSidebar() { - - - - - + {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/hooks/use-permission.ts b/resources/js/hooks/use-permission.ts new file mode 100644 index 0000000..1e1ce5c --- /dev/null +++ b/resources/js/hooks/use-permission.ts @@ -0,0 +1,17 @@ +import { usePage } from '@inertiajs/react'; +import type { Auth } from '@/types/auth'; + +export function usePermission() { + const { auth } = usePage<{auth: Auth}>().props; + + const hasPermission = (permission: string) => { + if (auth.roles.includes('Developer')) return true; + return auth.permissions.includes(permission); + }; + + const hasRole = (role: string) => { + return auth.roles.includes(role); + }; + + return { hasPermission, hasRole }; +} diff --git a/resources/js/pages/admin/finance/expense/index.tsx b/resources/js/pages/admin/finance/expense/index.tsx index 7a09fb4..f3010ed 100644 --- a/resources/js/pages/admin/finance/expense/index.tsx +++ b/resources/js/pages/admin/finance/expense/index.tsx @@ -10,8 +10,10 @@ import type { Expense } from '@/types'; import { useExpenseIndex } from './hooks/use-expense-index'; import { getColumns } from './partials/columns'; import { ExpenseFormModal } from './partials/expense-form-modal'; +import { usePermission } from '@/hooks/use-permission'; export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { + const { hasPermission } = usePermission(); const { selectedProof, isFormOpen, @@ -35,7 +37,9 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { closeImagePreview } = useExpenseIndex(); - const columns = getColumns({ onEdit, onDelete, onPreviewImage }); + const columns = getColumns({ onEdit, onDelete, onPreviewImage }).filter( + (col) => col.id !== 'actions' || hasPermission('Edit:Expense') || hasPermission('Delete:Expense') + ); return (
@@ -45,10 +49,12 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {

Pengeluaran

- + {hasPermission('Create:Expense') && ( + + )}
{ - setRowsToDelete(rows); - setIsBulkDeleteDialogOpen(true); - }, - icon: Trash2, - variant: 'destructive' - }, - ]} + bulkActions={ + hasPermission('DeleteAny:Expense') + ? [ + { + label: 'Hapus Terpilih', + onClick: (rows) => { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } /> diff --git a/resources/js/pages/admin/finance/expense/partials/columns.tsx b/resources/js/pages/admin/finance/expense/partials/columns.tsx index 514f815..6e152e6 100644 --- a/resources/js/pages/admin/finance/expense/partials/columns.tsx +++ b/resources/js/pages/admin/finance/expense/partials/columns.tsx @@ -1,3 +1,4 @@ +import { usePermission } from '@/hooks/use-permission'; import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2, ImagePlus } from 'lucide-react'; import { DataTableColumnHeader } from '@/components/data-table-column-header'; @@ -68,29 +69,34 @@ export const getColumns = ({ onEdit, onDelete, onPreviewImage }: ColumnProps): C header: "Aksi", cell: ({ row }) => { const expense = row.original; + const { hasPermission } = usePermission(); return (
- - - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
+ {hasPermission('Edit:Expense') && ( + + + + + +

Ubah

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

Hapus

+
+
+ )}
); }, diff --git a/resources/js/pages/admin/finance/payroll/index.tsx b/resources/js/pages/admin/finance/payroll/index.tsx index 45a53f0..a8add24 100644 --- a/resources/js/pages/admin/finance/payroll/index.tsx +++ b/resources/js/pages/admin/finance/payroll/index.tsx @@ -12,6 +12,7 @@ import type { Payroll } from '@/types'; import { usePayrollIndex } from './hooks/use-payroll-index'; import { getColumns } from './partials/columns'; import { PayrollFormModal } from './partials/payroll-form-modal'; +import { usePermission } from '@/hooks/use-permission'; export default function PayrollIndex({ payrolls, @@ -20,6 +21,7 @@ export default function PayrollIndex({ payrolls: Payroll[], currentMonthPayrolls: Payroll[], }) { + const { hasPermission } = usePermission(); const { isFormOpen, selectedPayroll, @@ -42,7 +44,9 @@ export default function PayrollIndex({ closeForm, } = usePayrollIndex(); - const columns = getColumns({ onEdit, onDelete, onTogglePaid }); + const columns = getColumns({ onEdit, onDelete, onTogglePaid }).filter( + (col) => col.id !== 'actions' || hasPermission('Edit:PayrollAdjustment') || hasPermission('Delete:Payroll') + ); return (
@@ -52,10 +56,12 @@ export default function PayrollIndex({

Penggajian

- + {hasPermission('Generate:Payroll') && ( + + )}
@@ -64,7 +70,11 @@ export default function PayrollIndex({ onEdit(payroll)} + onClick={() => { + if (hasPermission('Edit:PayrollAdjustment')) { + onEdit(payroll); + } + }} >
@@ -111,6 +121,7 @@ export default function PayrollIndex({ { - setRowsToDelete(rows); - setIsBulkDeleteDialogOpen(true); - }, - icon: Trash2, - variant: 'destructive' - }, - ]} + bulkActions={ + hasPermission('DeleteAny:Payroll') + ? [ + { + label: 'Hapus Terpilih', + onClick: (rows) => { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } /> diff --git a/resources/js/pages/admin/finance/payroll/partials/columns.tsx b/resources/js/pages/admin/finance/payroll/partials/columns.tsx index 99fd6b7..3194f07 100644 --- a/resources/js/pages/admin/finance/payroll/partials/columns.tsx +++ b/resources/js/pages/admin/finance/payroll/partials/columns.tsx @@ -1,3 +1,4 @@ +import { usePermission } from '@/hooks/use-permission'; import type { ColumnDef } from '@tanstack/react-table'; import { PencilRuler, Trash2 } from 'lucide-react'; import { DataTableColumnHeader } from '@/components/data-table-column-header'; @@ -72,12 +73,14 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col header: "Status", cell: ({ row }) => { const payroll = row.original; + const { hasPermission } = usePermission(); return (
onTogglePaid(payroll.id)} + disabled={!hasPermission('Pay:Payroll')} /> { const payroll = row.original; + const { hasPermission } = usePermission(); return (
- - - - - -

Ubah / Sesuaikan

-
-
- - - - - -

Hapus

-
-
+ {hasPermission('Edit:PayrollAdjustment') && ( + + + + + +

Ubah / Sesuaikan

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

Hapus

+
+
+ )}
); }, diff --git a/resources/js/pages/admin/manage/order/index.tsx b/resources/js/pages/admin/manage/order/index.tsx index ce7484b..48bd485 100644 --- a/resources/js/pages/admin/manage/order/index.tsx +++ b/resources/js/pages/admin/manage/order/index.tsx @@ -11,8 +11,10 @@ import { useOrderIndex } from './hooks/use-order-index'; import { useOrderPrint } from './hooks/use-order-print'; import { getColumns } from './partials/columns'; import { PrinterSettings } from './partials/printer-settings'; +import { usePermission } from '@/hooks/use-permission'; export default function OrderIndex({ orders }: { orders: Order[] }) { + const { hasPermission } = usePermission(); const { isDeleteDialogOpen, isBulkDeleteDialogOpen, @@ -39,7 +41,9 @@ export default function OrderIndex({ orders }: { orders: Order[] }) { handlePrint } = useOrderPrint(); - const columns = getColumns({ onDelete, onPrint: handlePrint }); + const columns = getColumns({ onDelete, onPrint: handlePrint }).filter( + (col) => col.id !== 'actions' || hasPermission('Edit:Order') || hasPermission('Delete:Order') + ); return (
@@ -60,12 +64,14 @@ export default function OrderIndex({ orders }: { orders: Order[] }) { onPaperSizeChange={setPaperSize} /> - - - + {hasPermission('Create:Order') && ( + + + + )}
@@ -74,19 +80,24 @@ export default function OrderIndex({ orders }: { orders: Order[] }) { { - setRowsToDelete(rows); - setIsBulkDeleteDialogOpen(true); - }, - icon: Trash2, - variant: 'destructive' - }, - ]} + bulkActions={ + hasPermission('DeleteAny:Order') + ? [ + { + label: 'Hapus Terpilih', + onClick: (rows) => { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } /> diff --git a/resources/js/pages/admin/manage/order/partials/columns.tsx b/resources/js/pages/admin/manage/order/partials/columns.tsx index dd1e292..40efdd2 100644 --- a/resources/js/pages/admin/manage/order/partials/columns.tsx +++ b/resources/js/pages/admin/manage/order/partials/columns.tsx @@ -1,3 +1,4 @@ +import { usePermission } from '@/hooks/use-permission'; import { Link } from '@inertiajs/react'; import type { ColumnDef } from '@tanstack/react-table'; import { format } from 'date-fns'; @@ -100,6 +101,7 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef header: "Aksi", cell: ({ row }) => { const order = row.original; + const { hasPermission } = usePermission(); return (
@@ -113,28 +115,32 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef

Cetak Struk

- - - - + + + +

Ubah

+
+
+ )} + {hasPermission('Delete:Order') && ( + + + - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
+ + +

Hapus

+
+ + )}
); }, diff --git a/resources/js/pages/admin/manage/purchase/index.tsx b/resources/js/pages/admin/manage/purchase/index.tsx index 2918f8a..4973355 100644 --- a/resources/js/pages/admin/manage/purchase/index.tsx +++ b/resources/js/pages/admin/manage/purchase/index.tsx @@ -12,8 +12,10 @@ import purchaseRoutes from '@/routes/purchase'; import type { Purchase } from '@/types'; import { usePurchaseIndex } from './hooks/use-purchase-index'; import { getColumns } from './partials/columns'; +import { usePermission } from '@/hooks/use-permission'; export default function PurchaseIndex({ purchases }: { purchases: Purchase[] }) { + const { hasPermission } = usePermission(); const { isDeleteDialogOpen, isBulkDeleteDialogOpen, @@ -29,7 +31,9 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] }) confirmBulkDelete, } = usePurchaseIndex(); - const columns = getColumns({ onDelete }); + const columns = getColumns({ onDelete }).filter( + (col) => col.id !== 'actions' || hasPermission('Edit:Purchase') || hasPermission('Delete:Purchase') + ); return (
@@ -39,12 +43,14 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] })

Belanja

- - - + {hasPermission('Create:Purchase') && ( + + + + )}
@@ -52,19 +58,24 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] }) { - setRowsToDelete(rows); - setIsBulkDeleteDialogOpen(true); - }, - icon: Trash2, - variant: 'destructive' - }, - ]} + bulkActions={ + hasPermission('DeleteAny:Purchase') + ? [ + { + label: 'Hapus Terpilih', + onClick: (rows) => { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } /> diff --git a/resources/js/pages/admin/manage/purchase/partials/columns.tsx b/resources/js/pages/admin/manage/purchase/partials/columns.tsx index 79a9da5..b38586d 100644 --- a/resources/js/pages/admin/manage/purchase/partials/columns.tsx +++ b/resources/js/pages/admin/manage/purchase/partials/columns.tsx @@ -1,3 +1,4 @@ +import { usePermission } from '@/hooks/use-permission'; import { Link } from '@inertiajs/react'; import type { ColumnDef } from '@tanstack/react-table'; import { format } from 'date-fns'; @@ -81,31 +82,36 @@ export const getColumns = ({ onDelete }: ColumnProps): ColumnDef[] => header: "Aksi", cell: ({ row }) => { const purchase = row.original; + const { hasPermission } = usePermission(); return (
- - - - + + + +

Ubah

+
+
+ )} + {hasPermission('Delete:Purchase') && ( + + + - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
+ + +

Hapus

+
+ + )}
); }, diff --git a/resources/js/pages/admin/master/category/index.tsx b/resources/js/pages/admin/master/category/index.tsx index 1bc1c14..ce66000 100644 --- a/resources/js/pages/admin/master/category/index.tsx +++ b/resources/js/pages/admin/master/category/index.tsx @@ -9,8 +9,10 @@ import type { Category } from '@/types'; import { useCategoryIndex } from './hooks/use-category-index'; import { CategoryFormModal } from './partials/category-form-modal'; import { getColumns } from './partials/columns'; +import { usePermission } from '@/hooks/use-permission'; export default function CategoryIndex({ categories }: { categories: Category[] }) { + const { hasPermission } = usePermission(); const { isFormOpen, selectedCategory, @@ -32,7 +34,9 @@ export default function CategoryIndex({ categories }: { categories: Category[] } closeForm, } = useCategoryIndex(); - const columns = getColumns({ onEdit, onDelete, onToggleStatus }); + const columns = getColumns({ onEdit, onDelete, onToggleStatus }).filter( + (col) => col.id !== 'actions' || hasPermission('Edit:Category') || hasPermission('Delete:Category') + ); return (
@@ -42,10 +46,12 @@ export default function CategoryIndex({ categories }: { categories: Category[] }

Kategori

- + {hasPermission('Create:Category') && ( + + )}
{ - setRowsToDelete(rows); - setIsBulkDeleteDialogOpen(true); - }, - icon: Trash2, - variant: 'destructive' - }, - ]} + bulkActions={ + hasPermission('DeleteAny:Category') + ? [ + { + label: 'Hapus Terpilih', + onClick: (rows) => { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } /> diff --git a/resources/js/pages/admin/master/category/partials/columns.tsx b/resources/js/pages/admin/master/category/partials/columns.tsx index 7dbf3a2..b611c34 100644 --- a/resources/js/pages/admin/master/category/partials/columns.tsx +++ b/resources/js/pages/admin/master/category/partials/columns.tsx @@ -1,3 +1,4 @@ +import { usePermission } from '@/hooks/use-permission'; import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; import { DataTableColumnHeader } from '@/components/data-table-column-header'; @@ -26,11 +27,13 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C meta: { title: "Status" }, cell: ({ row }) => { const category = row.original; + const { hasPermission } = usePermission(); return ( onToggleStatus(category.id)} + disabled={!hasPermission('ToggleStatus:Category')} /> ); } @@ -40,39 +43,44 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C header: "Aksi", cell: ({ row }) => { const category = row.original; + const { hasPermission } = usePermission(); return (
- - - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
+ {hasPermission('Edit:Category') && ( + + + + + +

Ubah

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

Hapus

+
+
+ )}
); }, diff --git a/resources/js/pages/admin/master/product/index.tsx b/resources/js/pages/admin/master/product/index.tsx index 28bb2fd..12c9ed0 100644 --- a/resources/js/pages/admin/master/product/index.tsx +++ b/resources/js/pages/admin/master/product/index.tsx @@ -10,8 +10,10 @@ import type { Product, Category } from '@/types'; import { ImagePreviewDialog } from '../../../../components/modal/image-preview'; import { useProductIndex } from './hooks/use-product-index'; import { getColumns } from './partials/columns'; +import { usePermission } from '@/hooks/use-permission'; export default function ProductIndex({ products, categories }: { products: Product[], categories: Category[] }) { + const { hasPermission } = usePermission(); const { selectedImage, isDeleteDialogOpen, @@ -31,7 +33,9 @@ export default function ProductIndex({ products, categories }: { products: Produ closeImagePreview, } = useProductIndex(); - const columns = getColumns({ onDelete, onToggleStatus, onPreviewImage }); + const columns = getColumns({ onDelete, onToggleStatus, onPreviewImage }).filter( + (col) => col.id !== 'actions' || hasPermission('Edit:Product') || hasPermission('Delete:Product') + ); return (
@@ -41,12 +45,14 @@ export default function ProductIndex({ products, categories }: { products: Produ

Produk

- - - + {hasPermission('Create:Product') && ( + + + + )}
@@ -54,6 +60,7 @@ export default function ProductIndex({ products, categories }: { products: Produ { - setRowsToDelete(rows); - setIsBulkDeleteDialogOpen(true); - }, - icon: Trash2, - variant: 'destructive' - }, - ]} + bulkActions={ + hasPermission('DeleteAny:Product') + ? [ + { + label: 'Hapus Terpilih', + onClick: (rows) => { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } /> diff --git a/resources/js/pages/admin/master/product/partials/columns.tsx b/resources/js/pages/admin/master/product/partials/columns.tsx index c31d695..ccbb92e 100644 --- a/resources/js/pages/admin/master/product/partials/columns.tsx +++ b/resources/js/pages/admin/master/product/partials/columns.tsx @@ -1,3 +1,4 @@ +import { usePermission } from '@/hooks/use-permission'; import { Link } from '@inertiajs/react'; import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2, ImagePlus } from 'lucide-react'; @@ -52,8 +53,8 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP const categories = row.getValue(id) as Category[]; if (!categories) { -return false; -} + return false; + } return categories.some(cat => String(cat.id) === String(value)); }, @@ -124,11 +125,13 @@ return false; meta: { title: "Status" }, cell: ({ row }) => { const product = row.original; + const { hasPermission } = usePermission(); return ( onToggleStatus(product.id)} + disabled={!hasPermission('ToggleStatus:Product')} /> ); } @@ -138,31 +141,36 @@ return false; header: "Aksi", cell: ({ row }) => { const product = row.original; + const { hasPermission } = usePermission(); return (
- - - - + + + +

Ubah

+
+
+ )} + {hasPermission('Delete:Product') && ( + + + - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
+ + +

Hapus

+
+ + )}
); }, diff --git a/resources/js/pages/admin/master/user/index.tsx b/resources/js/pages/admin/master/user/index.tsx index ef829fc..6b323b1 100644 --- a/resources/js/pages/admin/master/user/index.tsx +++ b/resources/js/pages/admin/master/user/index.tsx @@ -1,5 +1,3 @@ -import { Head, Link } from '@inertiajs/react'; -import { Trash2, KeyRound, Plus, X } from 'lucide-react'; import { DataTable } from '@/components/data-table'; import { DeleteConfirmation } from '@/components/modal/delete-confirmation'; import { @@ -12,16 +10,20 @@ import { AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, -} from "@/components/ui/alert-dialog" +} from "@/components/ui/alert-dialog"; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import userRoutes from '@/routes/user'; import type { User } from '@/types'; +import { Head, Link } from '@inertiajs/react'; +import { KeyRound, Plus, Trash2, X } from 'lucide-react'; +import { usePermission } from '@/hooks/use-permission'; import { useUserIndex } from './hooks/use-user-index'; import { getColumns } from './partials/columns'; export default function UserIndex({ users, defaultPassword }: { users: User[], defaultPassword: string }) { + const { hasPermission } = usePermission(); const { isDeleteDialogOpen, isBulkDeleteDialogOpen, @@ -42,7 +44,9 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d confirmBulkDelete, } = useUserIndex(); - const columns = getColumns({ onResetPassword, onDelete }); + const columns = getColumns({ onResetPassword, onDelete }).filter( + (col) => col.id !== 'actions' || hasPermission('Edit:User') || hasPermission('Delete:User') || hasPermission('ResetPassword:User') + ); return (
@@ -52,12 +56,14 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d

Pegawai

- - - + {hasPermission('Create:User') && ( + + + + )}
@@ -65,19 +71,24 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d { - setRowsToDelete(rows); - setIsBulkDeleteDialogOpen(true); - }, - icon: Trash2, - variant: 'destructive' - }, - ]} + bulkActions={ + hasPermission('DeleteAny:User') + ? [ + { + label: 'Hapus Terpilih', + onClick: (rows) => { + setRowsToDelete(rows); + setIsBulkDeleteDialogOpen(true); + }, + icon: Trash2, + variant: 'destructive', + }, + ] + : [] + } /> diff --git a/resources/js/pages/admin/master/user/partials/columns.tsx b/resources/js/pages/admin/master/user/partials/columns.tsx index 275523e..77ad482 100644 --- a/resources/js/pages/admin/master/user/partials/columns.tsx +++ b/resources/js/pages/admin/master/user/partials/columns.tsx @@ -1,12 +1,13 @@ -import { Link } from '@inertiajs/react'; -import type { ColumnDef } from '@tanstack/react-table'; -import { KeyRound, Pencil, Trash2 } from 'lucide-react'; import { DataTableColumnHeader } from '@/components/data-table-column-header'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { UserInfo } from '@/components/user-info'; +import { usePermission } from '@/hooks/use-permission'; import userRoutes from '@/routes/user'; import type { User } from '@/types'; +import { Link } from '@inertiajs/react'; +import type { ColumnDef } from '@tanstack/react-table'; +import { KeyRound, Pencil, Trash2 } from 'lucide-react'; interface ColumnProps { onResetPassword: (user: User) => void; @@ -50,55 +51,62 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe header: "Aksi", cell: ({ row }) => { const user = row.original; + const { hasPermission } = usePermission(); return (
- - - - - -

Reset Kata Sandi

-
-
- - - + {hasPermission('ResetPassword:User') && ( + + - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
+
+ +

Reset Kata Sandi

+
+
+ )} + {hasPermission('Edit:User') && ( + + + + + + + +

Ubah

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

Hapus

+
+
+ )}
); }, diff --git a/resources/js/types/auth.ts b/resources/js/types/auth.ts index 8086a2b..b2e24cc 100644 --- a/resources/js/types/auth.ts +++ b/resources/js/types/auth.ts @@ -27,6 +27,8 @@ export type User = { export type Auth = { user: User; + roles: string[]; + permissions: string[]; }; export type TwoFactorSetupData = { diff --git a/resources/js/types/navigation.ts b/resources/js/types/navigation.ts index dbf6f83..06b8f79 100644 --- a/resources/js/types/navigation.ts +++ b/resources/js/types/navigation.ts @@ -11,4 +11,5 @@ export type NavItem = { href: NonNullable; icon?: LucideIcon | null; isActive?: boolean; + permission?: string; }; diff --git a/routes/finance.php b/routes/finance.php index 6e680f4..c3245c3 100644 --- a/routes/finance.php +++ b/routes/finance.php @@ -6,17 +6,17 @@ Route::middleware(['auth'])->group(function () { Route::prefix('admin/finance')->group(function () { - Route::get('expenses', [ExpenseController::class, 'index'])->name('expense.index'); - Route::post('expense/store', [ExpenseController::class, 'store'])->name('expense.store'); - Route::patch('expense/update/{expense}', [ExpenseController::class, 'update'])->name('expense.update'); - Route::delete('expense/destroy/{expense}', [ExpenseController::class, 'destroy'])->name('expense.destroy'); - Route::delete('expense/bulk-destroy', [ExpenseController::class, 'bulkDestroy'])->name('expense.bulkDestroy'); + Route::get('expenses', [ExpenseController::class, 'index'])->name('expense.index')->middleware('can:View:Expense'); + Route::post('expense/store', [ExpenseController::class, 'store'])->name('expense.store')->middleware('can:Create:Expense'); + Route::patch('expense/update/{expense}', [ExpenseController::class, 'update'])->name('expense.update')->middleware('can:Edit:Expense'); + Route::delete('expense/destroy/{expense}', [ExpenseController::class, 'destroy'])->name('expense.destroy')->middleware('can:Delete:Expense'); + Route::delete('expense/bulk-destroy', [ExpenseController::class, 'bulkDestroy'])->name('expense.bulkDestroy')->middleware('can:DeleteAny:Expense'); - Route::get('payrolls', [PayrollController::class, 'index'])->name('payroll.index'); - Route::post('payroll/generate', [PayrollController::class, 'generate'])->name('payroll.generate'); - Route::patch('payroll/update/{payroll}', [PayrollController::class, 'update'])->name('payroll.update'); - Route::patch('payroll/toggle-paid/{payroll}', [PayrollController::class, 'togglePaid'])->name('payroll.togglePaid'); - Route::delete('payroll/destroy/{payroll}', [PayrollController::class, 'destroy'])->name('payroll.destroy'); - Route::delete('payroll/bulk-destroy', [PayrollController::class, 'bulkDestroy'])->name('payroll.bulkDestroy'); + Route::get('payrolls', [PayrollController::class, 'index'])->name('payroll.index')->middleware('can:View:Payroll'); + Route::post('payroll/generate', [PayrollController::class, 'generate'])->name('payroll.generate')->middleware('can:Generate:Payroll'); + Route::patch('payroll/update/{payroll}', [PayrollController::class, 'update'])->name('payroll.update')->middleware('can:Edit:PayrollAdjustment'); + Route::patch('payroll/toggle-paid/{payroll}', [PayrollController::class, 'togglePaid'])->name('payroll.togglePaid')->middleware('can:Pay:Payroll'); + Route::delete('payroll/destroy/{payroll}', [PayrollController::class, 'destroy'])->name('payroll.destroy')->middleware('can:Delete:Payroll'); + Route::delete('payroll/bulk-destroy', [PayrollController::class, 'bulkDestroy'])->name('payroll.bulkDestroy')->middleware('can:DeleteAny:Payroll'); }); }); diff --git a/routes/manage.php b/routes/manage.php index 461b319..a8f3350 100644 --- a/routes/manage.php +++ b/routes/manage.php @@ -8,30 +8,30 @@ Route::middleware(['auth'])->group(function () { Route::prefix('admin/manage')->group(function () { - Route::get('purchases', [PurchaseController::class, 'index'])->name('purchase.index'); - Route::get('purchase/create', [PurchaseController::class, 'create'])->name('purchase.create'); - Route::post('purchase/store', [PurchaseController::class, 'store'])->name('purchase.store'); - Route::get('purchase/edit/{purchase}', [PurchaseController::class, 'edit'])->name('purchase.edit'); - Route::patch('purchase/update/{purchase}', [PurchaseController::class, 'update'])->name('purchase.update'); - Route::delete('purchase/destroy/{purchase}', [PurchaseController::class, 'destroy'])->name('purchase.destroy'); - Route::delete('purchase/bulk-destroy', [PurchaseController::class, 'bulkDestroy'])->name('purchase.bulkDestroy'); + Route::get('purchases', [PurchaseController::class, 'index'])->name('purchase.index')->middleware('can:View:Purchase'); + Route::get('purchase/create', [PurchaseController::class, 'create'])->name('purchase.create')->middleware('can:Create:Purchase'); + Route::post('purchase/store', [PurchaseController::class, 'store'])->name('purchase.store')->middleware('can:Create:Purchase'); + Route::get('purchase/edit/{purchase}', [PurchaseController::class, 'edit'])->name('purchase.edit')->middleware('can:Edit:Purchase'); + Route::patch('purchase/update/{purchase}', [PurchaseController::class, 'update'])->name('purchase.update')->middleware('can:Edit:Purchase'); + Route::delete('purchase/destroy/{purchase}', [PurchaseController::class, 'destroy'])->name('purchase.destroy')->middleware('can:Delete:Purchase'); + Route::delete('purchase/bulk-destroy', [PurchaseController::class, 'bulkDestroy'])->name('purchase.bulkDestroy')->middleware('can:DeleteAny:Purchase'); // Cart routes - Route::post('purchase/add-to-cart', [PurchaseCartController::class, 'addToCart'])->name('purchase.addToCart'); - Route::delete('purchase/remove-from-cart/{purchaseItem}', [PurchaseCartController::class, 'removeFromCart'])->name('purchase.removeFromCart'); - Route::patch('purchase/update-cart-item/{purchaseItem}', [PurchaseCartController::class, 'updateCartItem'])->name('purchase.updateCartItem'); + Route::post('purchase/add-to-cart', [PurchaseCartController::class, 'addToCart'])->name('purchase.addToCart')->middleware('can:Create:Purchase'); + Route::delete('purchase/remove-from-cart/{purchaseItem}', [PurchaseCartController::class, 'removeFromCart'])->name('purchase.removeFromCart')->middleware('can:Create:Purchase'); + Route::patch('purchase/update-cart-item/{purchaseItem}', [PurchaseCartController::class, 'updateCartItem'])->name('purchase.updateCartItem')->middleware('can:Create:Purchase'); - Route::get('orders', [OrderController::class, 'index'])->name('order.index'); - Route::get('order/create', [OrderController::class, 'create'])->name('order.create'); - Route::post('order/store', [OrderController::class, 'store'])->name('order.store'); - Route::get('order/edit/{order}', [OrderController::class, 'edit'])->name('order.edit'); - Route::patch('order/update/{order}', [OrderController::class, 'update'])->name('order.update'); - Route::delete('order/destroy/{order}', [OrderController::class, 'destroy'])->name('order.destroy'); - Route::delete('order/bulk-destroy', [OrderController::class, 'bulkDestroy'])->name('order.bulkDestroy'); + Route::get('orders', [OrderController::class, 'index'])->name('order.index')->middleware('can:View:Order'); + Route::get('order/create', [OrderController::class, 'create'])->name('order.create')->middleware('can:Create:Order'); + Route::post('order/store', [OrderController::class, 'store'])->name('order.store')->middleware('can:Create:Order'); + Route::get('order/edit/{order}', [OrderController::class, 'edit'])->name('order.edit')->middleware('can:Edit:Order'); + Route::patch('order/update/{order}', [OrderController::class, 'update'])->name('order.update')->middleware('can:Edit:Order'); + Route::delete('order/destroy/{order}', [OrderController::class, 'destroy'])->name('order.destroy')->middleware('can:Delete:Order'); + Route::delete('order/bulk-destroy', [OrderController::class, 'bulkDestroy'])->name('order.bulkDestroy')->middleware('can:DeleteAny:Order'); // Order Cart routes - Route::post('order/add-to-cart', [OrderCartController::class, 'addToCart'])->name('order.addToCart'); - Route::delete('order/remove-from-cart/{orderItem}', [OrderCartController::class, 'removeFromCart'])->name('order.removeFromCart'); - Route::patch('order/update-cart-item/{orderItem}', [OrderCartController::class, 'updateCartItem'])->name('order.updateCartItem'); + Route::post('order/add-to-cart', [OrderCartController::class, 'addToCart'])->name('order.addToCart')->middleware('can:Create:Order'); + Route::delete('order/remove-from-cart/{orderItem}', [OrderCartController::class, 'removeFromCart'])->name('order.removeFromCart')->middleware('can:Create:Order'); + Route::patch('order/update-cart-item/{orderItem}', [OrderCartController::class, 'updateCartItem'])->name('order.updateCartItem')->middleware('can:Create:Order'); }); }); diff --git a/routes/master.php b/routes/master.php index 0a06398..0731626 100644 --- a/routes/master.php +++ b/routes/master.php @@ -7,29 +7,29 @@ Route::middleware(['auth'])->group(function () { Route::prefix('admin/master')->group(function () { - Route::get('categories', [CategoryController::class, 'index'])->name('category.index'); - Route::post('category/store', [CategoryController::class, 'store'])->name('category.store'); - Route::patch('category/update/{category}', [CategoryController::class, 'update'])->name('category.update'); - Route::delete('category/destroy/{category}', [CategoryController::class, 'destroy'])->name('category.destroy'); - Route::delete('category/bulk-destroy', [CategoryController::class, 'bulkDestroy'])->name('category.bulkDestroy'); - Route::patch('category/toggle-status/{category}', [CategoryController::class, 'toggleStatus'])->name('category.toggleStatus'); + Route::get('categories', [CategoryController::class, 'index'])->name('category.index')->middleware('can:View:Category'); + Route::post('category/store', [CategoryController::class, 'store'])->name('category.store')->middleware('can:Create:Category'); + Route::patch('category/update/{category}', [CategoryController::class, 'update'])->name('category.update')->middleware('can:Edit:Category'); + Route::delete('category/destroy/{category}', [CategoryController::class, 'destroy'])->name('category.destroy')->middleware('can:Delete:Category'); + Route::delete('category/bulk-destroy', [CategoryController::class, 'bulkDestroy'])->name('category.bulkDestroy')->middleware('can:DeleteAny:Category'); + Route::patch('category/toggle-status/{category}', [CategoryController::class, 'toggleStatus'])->name('category.toggleStatus')->middleware('can:ToggleStatus:Category'); - Route::get('products', [ProductController::class, 'index'])->name('product.index'); - Route::get('product/create', [ProductController::class, 'create'])->name('product.create'); - Route::post('product/store', [ProductController::class, 'store'])->name('product.store'); - Route::get('product/{product}/edit', [ProductController::class, 'edit'])->name('product.edit'); - Route::patch('product/update/{product}', [ProductController::class, 'update'])->name('product.update'); - Route::delete('product/destroy/{product}', [ProductController::class, 'destroy'])->name('product.destroy'); - Route::delete('product/bulk-destroy', [ProductController::class, 'bulkDestroy'])->name('product.bulkDestroy'); - Route::patch('product/toggle-status/{product}', [ProductController::class, 'toggleStatus'])->name('product.toggleStatus'); + Route::get('products', [ProductController::class, 'index'])->name('product.index')->middleware('can:View:Product'); + Route::get('product/create', [ProductController::class, 'create'])->name('product.create')->middleware('can:Create:Product'); + Route::post('product/store', [ProductController::class, 'store'])->name('product.store')->middleware('can:Create:Product'); + Route::get('product/{product}/edit', [ProductController::class, 'edit'])->name('product.edit')->middleware('can:Edit:Product'); + Route::patch('product/update/{product}', [ProductController::class, 'update'])->name('product.update')->middleware('can:Edit:Product'); + Route::delete('product/destroy/{product}', [ProductController::class, 'destroy'])->name('product.destroy')->middleware('can:Delete:Product'); + Route::delete('product/bulk-destroy', [ProductController::class, 'bulkDestroy'])->name('product.bulkDestroy')->middleware('can:DeleteAny:Product'); + Route::patch('product/toggle-status/{product}', [ProductController::class, 'toggleStatus'])->name('product.toggleStatus')->middleware('can:ToggleStatus:Product'); - Route::get('users', [UserController::class, 'index'])->name('user.index'); - Route::get('user/create', [UserController::class, 'create'])->name('user.create'); - Route::post('user/store', [UserController::class, 'store'])->name('user.store'); - Route::get('user/{user}/edit', [UserController::class, 'edit'])->name('user.edit'); - Route::patch('user/update/{user}', [UserController::class, 'update'])->name('user.update'); - Route::patch('user/reset-password/{user}', [UserController::class, 'resetPassword'])->name('user.resetPassword'); - Route::delete('user/destroy/{user}', [UserController::class, 'destroy'])->name('user.destroy'); - Route::delete('user/bulk-destroy', [UserController::class, 'bulkDestroy'])->name('user.bulkDestroy'); + Route::get('users', [UserController::class, 'index'])->name('user.index')->middleware('can:View:User'); + Route::get('user/create', [UserController::class, 'create'])->name('user.create')->middleware('can:Create:User'); + Route::post('user/store', [UserController::class, 'store'])->name('user.store')->middleware('can:Create:User'); + Route::get('user/{user}/edit', [UserController::class, 'edit'])->name('user.edit')->middleware('can:Edit:User'); + Route::patch('user/update/{user}', [UserController::class, 'update'])->name('user.update')->middleware('can:Edit:User'); + Route::patch('user/reset-password/{user}', [UserController::class, 'resetPassword'])->name('user.resetPassword')->middleware('can:ResetPassword:User'); + Route::delete('user/destroy/{user}', [UserController::class, 'destroy'])->name('user.destroy')->middleware('can:Delete:User'); + Route::delete('user/bulk-destroy', [UserController::class, 'bulkDestroy'])->name('user.bulkDestroy')->middleware('can:DeleteAny:User'); }); }); diff --git a/routes/system.php b/routes/system.php index 0643053..93fe8a5 100644 --- a/routes/system.php +++ b/routes/system.php @@ -7,12 +7,12 @@ Route::middleware(['auth'])->group(function () { Route::prefix('admin/system')->group(function () { - Route::get('logs', [LogController::class, 'index'])->name('system.logs.index'); - Route::get('activity-logs', [ActivityLogController::class, 'index'])->name('system.activity-logs.index'); + 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::controller(GeneralSettingController::class)->prefix('settings')->group(function () { - Route::get('/', 'index')->name('system.settings.index'); - Route::post('/', 'update')->name('system.settings.update'); + Route::get('/', 'index')->name('system.settings.index')->middleware('can:View:Setting'); + Route::post('/', 'update')->name('system.settings.update')->middleware('can:Edit:Setting'); }); }); }); diff --git a/routes/web.php b/routes/web.php index c74c013..19605a3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,8 +5,8 @@ use Illuminate\Support\Facades\Route; Route::middleware(['auth', 'verified'])->group(function () { - Route::get('dashboard', DashboardController::class)->name('dashboard'); - Route::get('analysis', AnalysisController::class)->name('analysis'); + Route::get('dashboard', DashboardController::class)->name('dashboard')->middleware('can:View:Dashboard'); + Route::get('analysis', AnalysisController::class)->name('analysis')->middleware('can:View:Analysis'); }); require __DIR__.'/settings.php';