feat: implement permission-based access control for routes and UI components using a new custom hook

This commit is contained in:
Yoga Pangestu 2026-04-28 18:52:47 +07:00
parent 9d0fc6d15e
commit 0fdd02e9e4
24 changed files with 554 additions and 378 deletions

View File

@ -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' => [

View File

@ -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 (
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader>
@ -113,11 +136,11 @@ export function AppSidebar() {
</SidebarHeader>
<SidebarContent>
<NavMain items={mainNavItems} />
<NavMain items={masterNavItems} label='Master' />
<NavMain items={manageNavItems} label='Kelola' />
<NavMain items={financeNavItems} label='Keuangan' />
<NavMain items={systemNavItems} label='Sistem' />
{filterNavItems(mainNavItems).length > 0 && <NavMain items={filterNavItems(mainNavItems)} />}
{filterNavItems(masterNavItems).length > 0 && <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>
</Sidebar>
);

View File

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

View File

@ -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 (
<div className="flex flex-col gap-6 p-6">
@ -45,10 +49,12 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
<div>
<h1 className="text-3xl font-bold tracking-tight">Pengeluaran</h1>
</div>
<Button onClick={onAdd} className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
{hasPermission('Create:Expense') && (
<Button onClick={onAdd} className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
)}
</div>
<ExpenseFormModal
@ -62,19 +68,24 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
<DataTable
columns={columns}
data={expenses}
showSelection={hasPermission('DeleteAny:Expense')}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
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',
},
]
: []
}
/>
</CardContent>
</Card>

View File

@ -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 (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-yellow-600' onClick={() => onEdit(expense)}>
<Pencil className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600' onClick={() => onDelete(expense)}>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
{hasPermission('Edit:Expense') && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-yellow-600' onClick={() => onEdit(expense)}>
<Pencil className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Delete:Expense') && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600' onClick={() => onDelete(expense)}>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
)}
</div>
);
},

View File

@ -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 (
<div className="flex flex-col gap-6 p-6">
@ -52,10 +56,12 @@ export default function PayrollIndex({
<div>
<h1 className="text-3xl font-bold tracking-tight">Penggajian</h1>
</div>
<Button onClick={onGenerate} disabled={isGenerating} className="gap-2">
{isGenerating ? <Sparkles className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
Generate
</Button>
{hasPermission('Generate:Payroll') && (
<Button onClick={onGenerate} disabled={isGenerating} className="gap-2">
{isGenerating ? <Sparkles className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
Generate
</Button>
)}
</div>
<div className="flex flex-col gap-3">
@ -64,7 +70,11 @@ export default function PayrollIndex({
<Card
key={payroll.id}
className="border-none shadow-lg bg-card/50 backdrop-blur-sm hover:scale-[1.02] transition-transform cursor-pointer"
onClick={() => onEdit(payroll)}
onClick={() => {
if (hasPermission('Edit:PayrollAdjustment')) {
onEdit(payroll);
}
}}
>
<CardContent className="p-4">
<div className="flex justify-between items-start mb-3">
@ -111,6 +121,7 @@ export default function PayrollIndex({
<DataTable
columns={columns}
data={payrolls}
showSelection={hasPermission('DeleteAny:Payroll')}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
filters={[
@ -123,17 +134,21 @@ export default function PayrollIndex({
]
}
]}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
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',
},
]
: []
}
/>
</CardContent>
</Card>

View File

@ -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 (
<div className="flex items-center gap-2">
<Switch
checked={payroll.is_paid}
onCheckedChange={() => onTogglePaid(payroll.id)}
disabled={!hasPermission('Pay:Payroll')}
/>
<Badge
variant={payroll.is_paid ? 'default' : 'outline'}
@ -95,39 +98,44 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
header: "Aksi",
cell: ({ row }) => {
const payroll = row.original;
const { hasPermission } = usePermission();
return (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-yellow-600'
onClick={() => onEdit(payroll)}
>
<PencilRuler className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Ubah / Sesuaikan</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-red-600'
onClick={() => onDelete(payroll)}
>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
{hasPermission('Edit:PayrollAdjustment') && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-yellow-600'
onClick={() => onEdit(payroll)}
>
<PencilRuler className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Ubah / Sesuaikan</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Delete:Payroll') && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-red-600'
onClick={() => onDelete(payroll)}
>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
)}
</div>
);
},

View File

@ -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 (
<div className="flex flex-col gap-6 p-6">
@ -60,12 +64,14 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
onPaperSizeChange={setPaperSize}
/>
<Link href={orderRoutes.create().url}>
<Button className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
</Link>
{hasPermission('Create:Order') && (
<Link href={orderRoutes.create().url}>
<Button className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
</Link>
)}
</div>
</div>
@ -74,19 +80,24 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
<DataTable
columns={columns}
data={orders}
showSelection={hasPermission('DeleteAny:Order')}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
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',
},
]
: []
}
/>
</CardContent>
</Card>

View File

@ -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<Order>
header: "Aksi",
cell: ({ row }) => {
const order = row.original;
const { hasPermission } = usePermission();
return (
<div className="flex items-center gap-2">
@ -113,28 +115,32 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
<p>Cetak Struk</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link href={orderRoutes.edit(order.id).url}>
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
<Pencil className="size-4" />
{hasPermission('Edit:Order') && (
<Tooltip>
<TooltipTrigger asChild>
<Link href={orderRoutes.edit(order.id).url}>
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
<Pencil className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Delete:Order') && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(order)}>
<Trash2 className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(order)}>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
)}
</div>
);
},

View File

@ -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 (
<div className="flex flex-col gap-6 p-6">
@ -39,12 +43,14 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] })
<div>
<h1 className="text-3xl font-bold tracking-tight">Belanja</h1>
</div>
<Link href={purchaseRoutes.create().url}>
<Button>
<Plus className="size-4" />
Tambah
</Button>
</Link>
{hasPermission('Create:Purchase') && (
<Link href={purchaseRoutes.create().url}>
<Button>
<Plus className="size-4" />
Tambah
</Button>
</Link>
)}
</div>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
@ -52,19 +58,24 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] })
<DataTable
columns={columns}
data={purchases}
showSelection={hasPermission('DeleteAny:Purchase')}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
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',
},
]
: []
}
/>
</CardContent>
</Card>

View File

@ -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<Purchase>[] =>
header: "Aksi",
cell: ({ row }) => {
const purchase = row.original;
const { hasPermission } = usePermission();
return (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Link href={purchaseRoutes.edit(purchase.id).url}>
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
<Pencil className="size-4" />
{hasPermission('Edit:Purchase') && (
<Tooltip>
<TooltipTrigger asChild>
<Link href={purchaseRoutes.edit(purchase.id).url}>
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
<Pencil className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Delete:Purchase') && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(purchase)}>
<Trash2 className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(purchase)}>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
)}
</div>
);
},

View File

@ -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 (
<div className="flex flex-col gap-6 p-6">
@ -42,10 +46,12 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
<div>
<h1 className="text-3xl font-bold tracking-tight">Kategori</h1>
</div>
<Button onClick={onAdd} className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
{hasPermission('Create:Category') && (
<Button onClick={onAdd} className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
)}
</div>
<CategoryFormModal
@ -59,6 +65,7 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
<DataTable
columns={columns}
data={categories}
showSelection={hasPermission('DeleteAny:Category')}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
filters={[
@ -71,17 +78,21 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
]
}
]}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
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',
},
]
: []
}
/>
</CardContent>
</Card>

View File

@ -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 (
<Switch
checked={category.is_active}
onCheckedChange={() => 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 (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-yellow-600'
onClick={() => onEdit(category)}
>
<Pencil className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-red-600'
onClick={() => onDelete(category)}
>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
{hasPermission('Edit:Category') && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-yellow-600'
onClick={() => onEdit(category)}
>
<Pencil className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Delete:Category') && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-red-600'
onClick={() => onDelete(category)}
>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
)}
</div>
);
},

View File

@ -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 (
<div className="flex flex-col gap-6 p-6">
@ -41,12 +45,14 @@ export default function ProductIndex({ products, categories }: { products: Produ
<div>
<h1 className="text-3xl font-bold tracking-tight">Produk</h1>
</div>
<Link href={productRoutes.create().url}>
<Button className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
</Link>
{hasPermission('Create:Product') && (
<Link href={productRoutes.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">
@ -54,6 +60,7 @@ export default function ProductIndex({ products, categories }: { products: Produ
<DataTable
columns={columns}
data={products}
showSelection={hasPermission('DeleteAny:Product')}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
filters={[
@ -74,17 +81,21 @@ export default function ProductIndex({ products, categories }: { products: Produ
})),
},
]}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
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',
},
]
: []
}
/>
</CardContent>
</Card>

View File

@ -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 (
<Switch
checked={product.is_active}
onCheckedChange={() => 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 (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Link href={productRoutes.edit(product.id).url}>
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
<Pencil className="size-4" />
{hasPermission('Edit:Product') && (
<Tooltip>
<TooltipTrigger asChild>
<Link href={productRoutes.edit(product.id).url}>
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
<Pencil className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Delete:Product') && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(product)}>
<Trash2 className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(product)}>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
)}
</div>
);
},

View File

@ -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 (
<div className="flex flex-col gap-6 p-6">
@ -52,12 +56,14 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d
<div>
<h1 className="text-3xl font-bold tracking-tight">Pegawai</h1>
</div>
<Link href={userRoutes.create().url}>
<Button className="gap-2">
<Plus className="size-4" />
Tambah
</Button>
</Link>
{hasPermission('Create:User') && (
<Link href={userRoutes.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">
@ -65,19 +71,24 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d
<DataTable
columns={columns}
data={users}
showSelection={hasPermission('DeleteAny:User')}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
bulkActions={[
{
label: 'Hapus Terpilih',
onClick: (rows) => {
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',
},
]
: []
}
/>
</CardContent>
</Card>

View File

@ -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 (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20'
onClick={() => onResetPassword(user)}
>
<KeyRound className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Reset Kata Sandi</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link href={userRoutes.edit(user.id).url}>
{hasPermission('ResetPassword:User') && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'
className='text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20'
onClick={() => onResetPassword(user)}
>
<Pencil className="size-4" />
<KeyRound className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20'
onClick={() => onDelete(user)}
>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
</TooltipTrigger>
<TooltipContent>
<p>Reset Kata Sandi</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Edit:User') && (
<Tooltip>
<TooltipTrigger asChild>
<Link href={userRoutes.edit(user.id).url}>
<Button
variant="ghost"
size="icon"
className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'
>
<Pencil className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Ubah</p>
</TooltipContent>
</Tooltip>
)}
{hasPermission('Delete:User') && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20'
onClick={() => onDelete(user)}
>
<Trash2 className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Hapus</p>
</TooltipContent>
</Tooltip>
)}
</div>
);
},

View File

@ -27,6 +27,8 @@ export type User = {
export type Auth = {
user: User;
roles: string[];
permissions: string[];
};
export type TwoFactorSetupData = {

View File

@ -11,4 +11,5 @@ export type NavItem = {
href: NonNullable<InertiaLinkProps['href']>;
icon?: LucideIcon | null;
isActive?: boolean;
permission?: string;
};

View File

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

View File

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

View File

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

View File

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

View File

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