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), ...parent::share($request),
'name' => config('app.name'), 'name' => config('app.name'),
'auth' => [ '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', 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'flash' => [ 'flash' => [

View File

@ -21,17 +21,21 @@ import purchase from '@/routes/purchase';
import system from '@/routes/system'; import system from '@/routes/system';
import user from '@/routes/user'; import user from '@/routes/user';
import type { NavItem } from '@/types'; import type { NavItem } from '@/types';
import { usePage } from '@inertiajs/react';
import type { Auth } from '@/types/auth';
const mainNavItems: NavItem[] = [ const mainNavItems: NavItem[] = [
{ {
title: 'Dasbor', title: 'Dasbor',
href: dashboard().url, href: dashboard().url,
icon: LayoutGrid, icon: LayoutGrid,
permission: 'View:Dashboard',
}, },
{ {
title: 'Analisa', title: 'Analisa',
href: analysis().url, href: analysis().url,
icon: BarChart3, icon: BarChart3,
permission: 'View:Analysis',
}, },
]; ];
@ -40,16 +44,19 @@ const masterNavItems: NavItem[] = [
title: 'Pegawai', title: 'Pegawai',
href: user.index().url, href: user.index().url,
icon: User, icon: User,
permission: 'View:User',
}, },
{ {
title: 'Kategori', title: 'Kategori',
href: category.index().url, href: category.index().url,
icon: List, icon: List,
permission: 'View:Category',
}, },
{ {
title: 'Produk', title: 'Produk',
href: product.index().url, href: product.index().url,
icon: Boxes, icon: Boxes,
permission: 'View:Product',
}, },
]; ];
@ -58,11 +65,13 @@ const manageNavItems: NavItem[] = [
title: 'Pesanan', title: 'Pesanan',
href: order.index().url, href: order.index().url,
icon: ShoppingBag, icon: ShoppingBag,
permission: 'View:Order',
}, },
{ {
title: 'Belanja', title: 'Belanja',
href: purchase.index().url, href: purchase.index().url,
icon: ShoppingCart, icon: ShoppingCart,
permission: 'View:Purchase',
}, },
]; ];
@ -71,11 +80,13 @@ const financeNavItems: NavItem[] = [
title: 'Pengeluaran', title: 'Pengeluaran',
href: expense.index().url, href: expense.index().url,
icon: Wallet, icon: Wallet,
permission: 'View:Expense',
}, },
{ {
title: 'Penggajian', title: 'Penggajian',
href: payroll.index().url, href: payroll.index().url,
icon: DollarSign, icon: DollarSign,
permission: 'View:Payroll',
}, },
]; ];
@ -84,20 +95,32 @@ const systemNavItems: NavItem[] = [
title: 'Pengaturan', title: 'Pengaturan',
href: system.settings.index().url, href: system.settings.index().url,
icon: Settings, icon: Settings,
permission: 'View:Setting',
}, },
{ {
title: 'Logs', title: 'Logs',
href: system.logs.index().url, href: system.logs.index().url,
icon: ScrollText, icon: ScrollText,
permission: 'View:Log',
}, },
{ {
title: 'Riwayat', title: 'Riwayat',
href: system.activityLogs.index().url, href: system.activityLogs.index().url,
icon: History, icon: History,
permission: 'View:Activity',
}, },
]; ];
export function AppSidebar() { 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 ( return (
<Sidebar collapsible="icon" variant="inset"> <Sidebar collapsible="icon" variant="inset">
<SidebarHeader> <SidebarHeader>
@ -113,11 +136,11 @@ export function AppSidebar() {
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={mainNavItems} /> {filterNavItems(mainNavItems).length > 0 && <NavMain items={filterNavItems(mainNavItems)} />}
<NavMain items={masterNavItems} label='Master' /> {filterNavItems(masterNavItems).length > 0 && <NavMain items={filterNavItems(masterNavItems)} label='Master' />}
<NavMain items={manageNavItems} label='Kelola' /> {filterNavItems(manageNavItems).length > 0 && <NavMain items={filterNavItems(manageNavItems)} label='Kelola' />}
<NavMain items={financeNavItems} label='Keuangan' /> {filterNavItems(financeNavItems).length > 0 && <NavMain items={filterNavItems(financeNavItems)} label='Keuangan' />}
<NavMain items={systemNavItems} label='Sistem' /> {filterNavItems(systemNavItems).length > 0 && <NavMain items={filterNavItems(systemNavItems)} label='Sistem' />}
</SidebarContent> </SidebarContent>
</Sidebar> </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 { useExpenseIndex } from './hooks/use-expense-index';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { ExpenseFormModal } from './partials/expense-form-modal'; import { ExpenseFormModal } from './partials/expense-form-modal';
import { usePermission } from '@/hooks/use-permission';
export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
const { hasPermission } = usePermission();
const { const {
selectedProof, selectedProof,
isFormOpen, isFormOpen,
@ -35,7 +37,9 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
closeImagePreview closeImagePreview
} = useExpenseIndex(); } = 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 ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
@ -45,10 +49,12 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Pengeluaran</h1> <h1 className="text-3xl font-bold tracking-tight">Pengeluaran</h1>
</div> </div>
{hasPermission('Create:Expense') && (
<Button onClick={onAdd} className="gap-2"> <Button onClick={onAdd} className="gap-2">
<Plus className="size-4" /> <Plus className="size-4" />
Tambah Tambah
</Button> </Button>
)}
</div> </div>
<ExpenseFormModal <ExpenseFormModal
@ -62,9 +68,12 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
<DataTable <DataTable
columns={columns} columns={columns}
data={expenses} data={expenses}
showSelection={hasPermission('DeleteAny:Expense')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
bulkActions={[ bulkActions={
hasPermission('DeleteAny:Expense')
? [
{ {
label: 'Hapus Terpilih', label: 'Hapus Terpilih',
onClick: (rows) => { onClick: (rows) => {
@ -72,9 +81,11 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {
setIsBulkDeleteDialogOpen(true); setIsBulkDeleteDialogOpen(true);
}, },
icon: Trash2, icon: Trash2,
variant: 'destructive' variant: 'destructive',
}, },
]} ]
: []
}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,3 +1,4 @@
import { usePermission } from '@/hooks/use-permission';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2, ImagePlus } from 'lucide-react'; import { Pencil, Trash2, ImagePlus } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header'; import { DataTableColumnHeader } from '@/components/data-table-column-header';
@ -68,9 +69,11 @@ export const getColumns = ({ onEdit, onDelete, onPreviewImage }: ColumnProps): C
header: "Aksi", header: "Aksi",
cell: ({ row }) => { cell: ({ row }) => {
const expense = row.original; const expense = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{hasPermission('Edit:Expense') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-yellow-600' onClick={() => onEdit(expense)}> <Button variant="ghost" size="icon" className='text-yellow-600' onClick={() => onEdit(expense)}>
@ -81,6 +84,8 @@ export const getColumns = ({ onEdit, onDelete, onPreviewImage }: ColumnProps): C
<p>Ubah</p> <p>Ubah</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Delete:Expense') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="ghost" size="icon" className='text-red-600' onClick={() => onDelete(expense)}> <Button variant="ghost" size="icon" className='text-red-600' onClick={() => onDelete(expense)}>
@ -91,6 +96,7 @@ export const getColumns = ({ onEdit, onDelete, onPreviewImage }: ColumnProps): C
<p>Hapus</p> <p>Hapus</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
</div> </div>
); );
}, },

View File

@ -12,6 +12,7 @@ import type { Payroll } from '@/types';
import { usePayrollIndex } from './hooks/use-payroll-index'; import { usePayrollIndex } from './hooks/use-payroll-index';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { PayrollFormModal } from './partials/payroll-form-modal'; import { PayrollFormModal } from './partials/payroll-form-modal';
import { usePermission } from '@/hooks/use-permission';
export default function PayrollIndex({ export default function PayrollIndex({
payrolls, payrolls,
@ -20,6 +21,7 @@ export default function PayrollIndex({
payrolls: Payroll[], payrolls: Payroll[],
currentMonthPayrolls: Payroll[], currentMonthPayrolls: Payroll[],
}) { }) {
const { hasPermission } = usePermission();
const { const {
isFormOpen, isFormOpen,
selectedPayroll, selectedPayroll,
@ -42,7 +44,9 @@ export default function PayrollIndex({
closeForm, closeForm,
} = usePayrollIndex(); } = 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 ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
@ -52,10 +56,12 @@ export default function PayrollIndex({
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Penggajian</h1> <h1 className="text-3xl font-bold tracking-tight">Penggajian</h1>
</div> </div>
{hasPermission('Generate:Payroll') && (
<Button onClick={onGenerate} disabled={isGenerating} className="gap-2"> <Button onClick={onGenerate} disabled={isGenerating} className="gap-2">
{isGenerating ? <Sparkles className="size-4 animate-spin" /> : <Sparkles className="size-4" />} {isGenerating ? <Sparkles className="size-4 animate-spin" /> : <Sparkles className="size-4" />}
Generate Generate
</Button> </Button>
)}
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@ -64,7 +70,11 @@ export default function PayrollIndex({
<Card <Card
key={payroll.id} key={payroll.id}
className="border-none shadow-lg bg-card/50 backdrop-blur-sm hover:scale-[1.02] transition-transform cursor-pointer" 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"> <CardContent className="p-4">
<div className="flex justify-between items-start mb-3"> <div className="flex justify-between items-start mb-3">
@ -111,6 +121,7 @@ export default function PayrollIndex({
<DataTable <DataTable
columns={columns} columns={columns}
data={payrolls} data={payrolls}
showSelection={hasPermission('DeleteAny:Payroll')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
filters={[ filters={[
@ -123,7 +134,9 @@ export default function PayrollIndex({
] ]
} }
]} ]}
bulkActions={[ bulkActions={
hasPermission('DeleteAny:Payroll')
? [
{ {
label: 'Hapus Terpilih', label: 'Hapus Terpilih',
onClick: (rows) => { onClick: (rows) => {
@ -131,9 +144,11 @@ export default function PayrollIndex({
setIsBulkDeleteDialogOpen(true); setIsBulkDeleteDialogOpen(true);
}, },
icon: Trash2, icon: Trash2,
variant: 'destructive' variant: 'destructive',
}, },
]} ]
: []
}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,3 +1,4 @@
import { usePermission } from '@/hooks/use-permission';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { PencilRuler, Trash2 } from 'lucide-react'; import { PencilRuler, Trash2 } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header'; import { DataTableColumnHeader } from '@/components/data-table-column-header';
@ -72,12 +73,14 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
header: "Status", header: "Status",
cell: ({ row }) => { cell: ({ row }) => {
const payroll = row.original; const payroll = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch
checked={payroll.is_paid} checked={payroll.is_paid}
onCheckedChange={() => onTogglePaid(payroll.id)} onCheckedChange={() => onTogglePaid(payroll.id)}
disabled={!hasPermission('Pay:Payroll')}
/> />
<Badge <Badge
variant={payroll.is_paid ? 'default' : 'outline'} variant={payroll.is_paid ? 'default' : 'outline'}
@ -95,9 +98,11 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
header: "Aksi", header: "Aksi",
cell: ({ row }) => { cell: ({ row }) => {
const payroll = row.original; const payroll = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{hasPermission('Edit:PayrollAdjustment') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@ -113,6 +118,8 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
<p>Ubah / Sesuaikan</p> <p>Ubah / Sesuaikan</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Delete:Payroll') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@ -128,6 +135,7 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
<p>Hapus</p> <p>Hapus</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
</div> </div>
); );
}, },

View File

@ -11,8 +11,10 @@ import { useOrderIndex } from './hooks/use-order-index';
import { useOrderPrint } from './hooks/use-order-print'; import { useOrderPrint } from './hooks/use-order-print';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { PrinterSettings } from './partials/printer-settings'; import { PrinterSettings } from './partials/printer-settings';
import { usePermission } from '@/hooks/use-permission';
export default function OrderIndex({ orders }: { orders: Order[] }) { export default function OrderIndex({ orders }: { orders: Order[] }) {
const { hasPermission } = usePermission();
const { const {
isDeleteDialogOpen, isDeleteDialogOpen,
isBulkDeleteDialogOpen, isBulkDeleteDialogOpen,
@ -39,7 +41,9 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
handlePrint handlePrint
} = useOrderPrint(); } = 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 ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
@ -60,12 +64,14 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
onPaperSizeChange={setPaperSize} onPaperSizeChange={setPaperSize}
/> />
{hasPermission('Create:Order') && (
<Link href={orderRoutes.create().url}> <Link href={orderRoutes.create().url}>
<Button className="gap-2"> <Button className="gap-2">
<Plus className="size-4" /> <Plus className="size-4" />
Tambah Tambah
</Button> </Button>
</Link> </Link>
)}
</div> </div>
</div> </div>
@ -74,9 +80,12 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
<DataTable <DataTable
columns={columns} columns={columns}
data={orders} data={orders}
showSelection={hasPermission('DeleteAny:Order')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
bulkActions={[ bulkActions={
hasPermission('DeleteAny:Order')
? [
{ {
label: 'Hapus Terpilih', label: 'Hapus Terpilih',
onClick: (rows) => { onClick: (rows) => {
@ -84,9 +93,11 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
setIsBulkDeleteDialogOpen(true); setIsBulkDeleteDialogOpen(true);
}, },
icon: Trash2, icon: Trash2,
variant: 'destructive' variant: 'destructive',
}, },
]} ]
: []
}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,3 +1,4 @@
import { usePermission } from '@/hooks/use-permission';
import { Link } from '@inertiajs/react'; import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -100,6 +101,7 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
header: "Aksi", header: "Aksi",
cell: ({ row }) => { cell: ({ row }) => {
const order = row.original; const order = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -113,6 +115,7 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
<p>Cetak Struk</p> <p>Cetak Struk</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
{hasPermission('Edit:Order') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Link href={orderRoutes.edit(order.id).url}> <Link href={orderRoutes.edit(order.id).url}>
@ -125,6 +128,8 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
<p>Ubah</p> <p>Ubah</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Delete:Order') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <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)}> <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)}>
@ -135,6 +140,7 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
<p>Hapus</p> <p>Hapus</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
</div> </div>
); );
}, },

View File

@ -12,8 +12,10 @@ import purchaseRoutes from '@/routes/purchase';
import type { Purchase } from '@/types'; import type { Purchase } from '@/types';
import { usePurchaseIndex } from './hooks/use-purchase-index'; import { usePurchaseIndex } from './hooks/use-purchase-index';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { usePermission } from '@/hooks/use-permission';
export default function PurchaseIndex({ purchases }: { purchases: Purchase[] }) { export default function PurchaseIndex({ purchases }: { purchases: Purchase[] }) {
const { hasPermission } = usePermission();
const { const {
isDeleteDialogOpen, isDeleteDialogOpen,
isBulkDeleteDialogOpen, isBulkDeleteDialogOpen,
@ -29,7 +31,9 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] })
confirmBulkDelete, confirmBulkDelete,
} = usePurchaseIndex(); } = usePurchaseIndex();
const columns = getColumns({ onDelete }); const columns = getColumns({ onDelete }).filter(
(col) => col.id !== 'actions' || hasPermission('Edit:Purchase') || hasPermission('Delete:Purchase')
);
return ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
@ -39,12 +43,14 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] })
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Belanja</h1> <h1 className="text-3xl font-bold tracking-tight">Belanja</h1>
</div> </div>
{hasPermission('Create:Purchase') && (
<Link href={purchaseRoutes.create().url}> <Link href={purchaseRoutes.create().url}>
<Button> <Button>
<Plus className="size-4" /> <Plus className="size-4" />
Tambah Tambah
</Button> </Button>
</Link> </Link>
)}
</div> </div>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5"> <Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
@ -52,9 +58,12 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] })
<DataTable <DataTable
columns={columns} columns={columns}
data={purchases} data={purchases}
showSelection={hasPermission('DeleteAny:Purchase')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
bulkActions={[ bulkActions={
hasPermission('DeleteAny:Purchase')
? [
{ {
label: 'Hapus Terpilih', label: 'Hapus Terpilih',
onClick: (rows) => { onClick: (rows) => {
@ -62,9 +71,11 @@ export default function PurchaseIndex({ purchases }: { purchases: Purchase[] })
setIsBulkDeleteDialogOpen(true); setIsBulkDeleteDialogOpen(true);
}, },
icon: Trash2, icon: Trash2,
variant: 'destructive' variant: 'destructive',
}, },
]} ]
: []
}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,3 +1,4 @@
import { usePermission } from '@/hooks/use-permission';
import { Link } from '@inertiajs/react'; import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -81,9 +82,11 @@ export const getColumns = ({ onDelete }: ColumnProps): ColumnDef<Purchase>[] =>
header: "Aksi", header: "Aksi",
cell: ({ row }) => { cell: ({ row }) => {
const purchase = row.original; const purchase = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{hasPermission('Edit:Purchase') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Link href={purchaseRoutes.edit(purchase.id).url}> <Link href={purchaseRoutes.edit(purchase.id).url}>
@ -96,6 +99,8 @@ export const getColumns = ({ onDelete }: ColumnProps): ColumnDef<Purchase>[] =>
<p>Ubah</p> <p>Ubah</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Delete:Purchase') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <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)}> <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)}>
@ -106,6 +111,7 @@ export const getColumns = ({ onDelete }: ColumnProps): ColumnDef<Purchase>[] =>
<p>Hapus</p> <p>Hapus</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
</div> </div>
); );
}, },

View File

@ -9,8 +9,10 @@ import type { Category } from '@/types';
import { useCategoryIndex } from './hooks/use-category-index'; import { useCategoryIndex } from './hooks/use-category-index';
import { CategoryFormModal } from './partials/category-form-modal'; import { CategoryFormModal } from './partials/category-form-modal';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { usePermission } from '@/hooks/use-permission';
export default function CategoryIndex({ categories }: { categories: Category[] }) { export default function CategoryIndex({ categories }: { categories: Category[] }) {
const { hasPermission } = usePermission();
const { const {
isFormOpen, isFormOpen,
selectedCategory, selectedCategory,
@ -32,7 +34,9 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
closeForm, closeForm,
} = useCategoryIndex(); } = 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 ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
@ -42,10 +46,12 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Kategori</h1> <h1 className="text-3xl font-bold tracking-tight">Kategori</h1>
</div> </div>
{hasPermission('Create:Category') && (
<Button onClick={onAdd} className="gap-2"> <Button onClick={onAdd} className="gap-2">
<Plus className="size-4" /> <Plus className="size-4" />
Tambah Tambah
</Button> </Button>
)}
</div> </div>
<CategoryFormModal <CategoryFormModal
@ -59,6 +65,7 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
<DataTable <DataTable
columns={columns} columns={columns}
data={categories} data={categories}
showSelection={hasPermission('DeleteAny:Category')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
filters={[ filters={[
@ -71,7 +78,9 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
] ]
} }
]} ]}
bulkActions={[ bulkActions={
hasPermission('DeleteAny:Category')
? [
{ {
label: 'Hapus Terpilih', label: 'Hapus Terpilih',
onClick: (rows) => { onClick: (rows) => {
@ -79,9 +88,11 @@ export default function CategoryIndex({ categories }: { categories: Category[] }
setIsBulkDeleteDialogOpen(true); setIsBulkDeleteDialogOpen(true);
}, },
icon: Trash2, icon: Trash2,
variant: 'destructive' variant: 'destructive',
}, },
]} ]
: []
}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,3 +1,4 @@
import { usePermission } from '@/hooks/use-permission';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react'; import { Pencil, Trash2 } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header'; import { DataTableColumnHeader } from '@/components/data-table-column-header';
@ -26,11 +27,13 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C
meta: { title: "Status" }, meta: { title: "Status" },
cell: ({ row }) => { cell: ({ row }) => {
const category = row.original; const category = row.original;
const { hasPermission } = usePermission();
return ( return (
<Switch <Switch
checked={category.is_active} checked={category.is_active}
onCheckedChange={() => onToggleStatus(category.id)} onCheckedChange={() => onToggleStatus(category.id)}
disabled={!hasPermission('ToggleStatus:Category')}
/> />
); );
} }
@ -40,9 +43,11 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C
header: "Aksi", header: "Aksi",
cell: ({ row }) => { cell: ({ row }) => {
const category = row.original; const category = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{hasPermission('Edit:Category') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@ -58,6 +63,8 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C
<p>Ubah</p> <p>Ubah</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Delete:Category') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@ -73,6 +80,7 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C
<p>Hapus</p> <p>Hapus</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
</div> </div>
); );
}, },

View File

@ -10,8 +10,10 @@ import type { Product, Category } from '@/types';
import { ImagePreviewDialog } from '../../../../components/modal/image-preview'; import { ImagePreviewDialog } from '../../../../components/modal/image-preview';
import { useProductIndex } from './hooks/use-product-index'; import { useProductIndex } from './hooks/use-product-index';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
import { usePermission } from '@/hooks/use-permission';
export default function ProductIndex({ products, categories }: { products: Product[], categories: Category[] }) { export default function ProductIndex({ products, categories }: { products: Product[], categories: Category[] }) {
const { hasPermission } = usePermission();
const { const {
selectedImage, selectedImage,
isDeleteDialogOpen, isDeleteDialogOpen,
@ -31,7 +33,9 @@ export default function ProductIndex({ products, categories }: { products: Produ
closeImagePreview, closeImagePreview,
} = useProductIndex(); } = 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 ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
@ -41,12 +45,14 @@ export default function ProductIndex({ products, categories }: { products: Produ
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Produk</h1> <h1 className="text-3xl font-bold tracking-tight">Produk</h1>
</div> </div>
{hasPermission('Create:Product') && (
<Link href={productRoutes.create().url}> <Link href={productRoutes.create().url}>
<Button className="gap-2"> <Button className="gap-2">
<Plus className="size-4" /> <Plus className="size-4" />
Tambah Tambah
</Button> </Button>
</Link> </Link>
)}
</div> </div>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5"> <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 <DataTable
columns={columns} columns={columns}
data={products} data={products}
showSelection={hasPermission('DeleteAny:Product')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
filters={[ filters={[
@ -74,7 +81,9 @@ export default function ProductIndex({ products, categories }: { products: Produ
})), })),
}, },
]} ]}
bulkActions={[ bulkActions={
hasPermission('DeleteAny:Product')
? [
{ {
label: 'Hapus Terpilih', label: 'Hapus Terpilih',
onClick: (rows) => { onClick: (rows) => {
@ -82,9 +91,11 @@ export default function ProductIndex({ products, categories }: { products: Produ
setIsBulkDeleteDialogOpen(true); setIsBulkDeleteDialogOpen(true);
}, },
icon: Trash2, icon: Trash2,
variant: 'destructive' variant: 'destructive',
}, },
]} ]
: []
}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,3 +1,4 @@
import { usePermission } from '@/hooks/use-permission';
import { Link } from '@inertiajs/react'; import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2, ImagePlus } from 'lucide-react'; 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[]; const categories = row.getValue(id) as Category[];
if (!categories) { if (!categories) {
return false; return false;
} }
return categories.some(cat => String(cat.id) === String(value)); return categories.some(cat => String(cat.id) === String(value));
}, },
@ -124,11 +125,13 @@ return false;
meta: { title: "Status" }, meta: { title: "Status" },
cell: ({ row }) => { cell: ({ row }) => {
const product = row.original; const product = row.original;
const { hasPermission } = usePermission();
return ( return (
<Switch <Switch
checked={product.is_active} checked={product.is_active}
onCheckedChange={() => onToggleStatus(product.id)} onCheckedChange={() => onToggleStatus(product.id)}
disabled={!hasPermission('ToggleStatus:Product')}
/> />
); );
} }
@ -138,9 +141,11 @@ return false;
header: "Aksi", header: "Aksi",
cell: ({ row }) => { cell: ({ row }) => {
const product = row.original; const product = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{hasPermission('Edit:Product') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Link href={productRoutes.edit(product.id).url}> <Link href={productRoutes.edit(product.id).url}>
@ -153,6 +158,8 @@ return false;
<p>Ubah</p> <p>Ubah</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Delete:Product') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <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)}> <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)}>
@ -163,6 +170,7 @@ return false;
<p>Hapus</p> <p>Hapus</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
</div> </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 { DataTable } from '@/components/data-table';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation'; import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { import {
@ -12,16 +10,20 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogMedia, AlertDialogMedia,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog";
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import userRoutes from '@/routes/user'; import userRoutes from '@/routes/user';
import type { User } from '@/types'; 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 { useUserIndex } from './hooks/use-user-index';
import { getColumns } from './partials/columns'; import { getColumns } from './partials/columns';
export default function UserIndex({ users, defaultPassword }: { users: User[], defaultPassword: string }) { export default function UserIndex({ users, defaultPassword }: { users: User[], defaultPassword: string }) {
const { hasPermission } = usePermission();
const { const {
isDeleteDialogOpen, isDeleteDialogOpen,
isBulkDeleteDialogOpen, isBulkDeleteDialogOpen,
@ -42,7 +44,9 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d
confirmBulkDelete, confirmBulkDelete,
} = useUserIndex(); } = 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 ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
@ -52,12 +56,14 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Pegawai</h1> <h1 className="text-3xl font-bold tracking-tight">Pegawai</h1>
</div> </div>
{hasPermission('Create:User') && (
<Link href={userRoutes.create().url}> <Link href={userRoutes.create().url}>
<Button className="gap-2"> <Button className="gap-2">
<Plus className="size-4" /> <Plus className="size-4" />
Tambah Tambah
</Button> </Button>
</Link> </Link>
)}
</div> </div>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5"> <Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
@ -65,9 +71,12 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d
<DataTable <DataTable
columns={columns} columns={columns}
data={users} data={users}
showSelection={hasPermission('DeleteAny:User')}
rowSelection={rowSelection} rowSelection={rowSelection}
onRowSelectionChange={setRowSelection} onRowSelectionChange={setRowSelection}
bulkActions={[ bulkActions={
hasPermission('DeleteAny:User')
? [
{ {
label: 'Hapus Terpilih', label: 'Hapus Terpilih',
onClick: (rows) => { onClick: (rows) => {
@ -75,9 +84,11 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d
setIsBulkDeleteDialogOpen(true); setIsBulkDeleteDialogOpen(true);
}, },
icon: Trash2, icon: Trash2,
variant: 'destructive' variant: 'destructive',
}, },
]} ]
: []
}
/> />
</CardContent> </CardContent>
</Card> </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 { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { UserInfo } from '@/components/user-info'; import { UserInfo } from '@/components/user-info';
import { usePermission } from '@/hooks/use-permission';
import userRoutes from '@/routes/user'; import userRoutes from '@/routes/user';
import type { User } from '@/types'; 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 { interface ColumnProps {
onResetPassword: (user: User) => void; onResetPassword: (user: User) => void;
@ -50,9 +51,11 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe
header: "Aksi", header: "Aksi",
cell: ({ row }) => { cell: ({ row }) => {
const user = row.original; const user = row.original;
const { hasPermission } = usePermission();
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{hasPermission('ResetPassword:User') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@ -68,6 +71,8 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe
<p>Reset Kata Sandi</p> <p>Reset Kata Sandi</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Edit:User') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Link href={userRoutes.edit(user.id).url}> <Link href={userRoutes.edit(user.id).url}>
@ -84,6 +89,8 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe
<p>Ubah</p> <p>Ubah</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
{hasPermission('Delete:User') && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@ -99,6 +106,7 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe
<p>Hapus</p> <p>Hapus</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)}
</div> </div>
); );
}, },

View File

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

View File

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

View File

@ -6,17 +6,17 @@
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
Route::prefix('admin/finance')->group(function () { Route::prefix('admin/finance')->group(function () {
Route::get('expenses', [ExpenseController::class, 'index'])->name('expense.index'); Route::get('expenses', [ExpenseController::class, 'index'])->name('expense.index')->middleware('can:View:Expense');
Route::post('expense/store', [ExpenseController::class, 'store'])->name('expense.store'); 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'); 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'); 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'); 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::get('payrolls', [PayrollController::class, 'index'])->name('payroll.index')->middleware('can:View:Payroll');
Route::post('payroll/generate', [PayrollController::class, 'generate'])->name('payroll.generate'); 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'); 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'); 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'); 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'); 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::middleware(['auth'])->group(function () {
Route::prefix('admin/manage')->group(function () { Route::prefix('admin/manage')->group(function () {
Route::get('purchases', [PurchaseController::class, 'index'])->name('purchase.index'); Route::get('purchases', [PurchaseController::class, 'index'])->name('purchase.index')->middleware('can:View:Purchase');
Route::get('purchase/create', [PurchaseController::class, 'create'])->name('purchase.create'); Route::get('purchase/create', [PurchaseController::class, 'create'])->name('purchase.create')->middleware('can:Create:Purchase');
Route::post('purchase/store', [PurchaseController::class, 'store'])->name('purchase.store'); 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'); 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'); 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'); 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'); Route::delete('purchase/bulk-destroy', [PurchaseController::class, 'bulkDestroy'])->name('purchase.bulkDestroy')->middleware('can:DeleteAny:Purchase');
// Cart routes // Cart routes
Route::post('purchase/add-to-cart', [PurchaseCartController::class, 'addToCart'])->name('purchase.addToCart'); 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'); 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'); 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('orders', [OrderController::class, 'index'])->name('order.index')->middleware('can:View:Order');
Route::get('order/create', [OrderController::class, 'create'])->name('order.create'); Route::get('order/create', [OrderController::class, 'create'])->name('order.create')->middleware('can:Create:Order');
Route::post('order/store', [OrderController::class, 'store'])->name('order.store'); 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'); 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'); 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'); 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'); Route::delete('order/bulk-destroy', [OrderController::class, 'bulkDestroy'])->name('order.bulkDestroy')->middleware('can:DeleteAny:Order');
// Order Cart routes // Order Cart routes
Route::post('order/add-to-cart', [OrderCartController::class, 'addToCart'])->name('order.addToCart'); 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'); 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'); 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::middleware(['auth'])->group(function () {
Route::prefix('admin/master')->group(function () { Route::prefix('admin/master')->group(function () {
Route::get('categories', [CategoryController::class, 'index'])->name('category.index'); Route::get('categories', [CategoryController::class, 'index'])->name('category.index')->middleware('can:View:Category');
Route::post('category/store', [CategoryController::class, 'store'])->name('category.store'); 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'); 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'); 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'); 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'); 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('products', [ProductController::class, 'index'])->name('product.index')->middleware('can:View:Product');
Route::get('product/create', [ProductController::class, 'create'])->name('product.create'); Route::get('product/create', [ProductController::class, 'create'])->name('product.create')->middleware('can:Create:Product');
Route::post('product/store', [ProductController::class, 'store'])->name('product.store'); 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'); 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'); 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'); 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'); 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'); 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('users', [UserController::class, 'index'])->name('user.index')->middleware('can:View:User');
Route::get('user/create', [UserController::class, 'create'])->name('user.create'); Route::get('user/create', [UserController::class, 'create'])->name('user.create')->middleware('can:Create:User');
Route::post('user/store', [UserController::class, 'store'])->name('user.store'); 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'); 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'); 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'); 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'); 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'); 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::middleware(['auth'])->group(function () {
Route::prefix('admin/system')->group(function () { Route::prefix('admin/system')->group(function () {
Route::get('logs', [LogController::class, 'index'])->name('system.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'); 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::controller(GeneralSettingController::class)->prefix('settings')->group(function () {
Route::get('/', 'index')->name('system.settings.index'); Route::get('/', 'index')->name('system.settings.index')->middleware('can:View:Setting');
Route::post('/', 'update')->name('system.settings.update'); Route::post('/', 'update')->name('system.settings.update')->middleware('can:Edit:Setting');
}); });
}); });
}); });

View File

@ -5,8 +5,8 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'verified'])->group(function () { Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', DashboardController::class)->name('dashboard'); Route::get('dashboard', DashboardController::class)->name('dashboard')->middleware('can:View:Dashboard');
Route::get('analysis', AnalysisController::class)->name('analysis'); Route::get('analysis', AnalysisController::class)->name('analysis')->middleware('can:View:Analysis');
}); });
require __DIR__.'/settings.php'; require __DIR__.'/settings.php';