feat: refactor column definitions to conditionally include action buttons based on user permissions

This commit is contained in:
Yoga Pangestu 2026-08-06 22:48:00 +07:00
parent 5ec0e9ff3b
commit 9bbb00c69f
20 changed files with 423 additions and 331 deletions

View File

@ -20,7 +20,7 @@ export function createCashAccountColumns(
): ColumnDef<CashAccount>[] {
const { handleEdit, handleDeleteClick, can } = params;
return [
const columns: ColumnDef<CashAccount>[] = [
{
accessorKey: 'name',
header: () => <span>Nama</span>,
@ -39,7 +39,10 @@ export function createCashAccountColumns(
</span>
),
},
{
];
if (can('cash.update') || can('cash.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -70,6 +73,8 @@ export function createCashAccountColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -47,7 +47,7 @@ export function createTransactionColumns(
): ColumnDef<CashTransaction>[] {
const { handleEdit, handleDeleteClick, can } = params;
return [
const columns: ColumnDef<CashTransaction>[] = [
{
accessorKey: 'formatted_created_at',
header: () => <span>Tanggal</span>,
@ -136,7 +136,10 @@ export function createTransactionColumns(
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
},
},
{
];
if (can('cash.update') || can('cash.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -174,6 +177,8 @@ export function createTransactionColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -91,7 +91,7 @@ export function createEmployeeAdvanceColumns(
const { handleEdit, handleDeleteClick, handleApprove, handlePay, handleShowPayments, can, authUserId } =
params;
return [
const columns: ColumnDef<EmployeeAdvance>[] = [
{
accessorKey: 'formatted_created_at',
header: () => <span>Tanggal</span>,
@ -162,7 +162,16 @@ export function createEmployeeAdvanceColumns(
<span>{getStatusBadge(row.getValue('status') as string)}</span>
),
},
{
];
if (
can('employee_advances.verify') ||
can('employee_advances.pay') ||
can('employee_advances.view_payments') ||
can('employee_advances.update') ||
can('employee_advances.delete')
) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -226,6 +235,8 @@ export function createEmployeeAdvanceColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -30,7 +30,7 @@ export function createExpenseColumns(
): ColumnDef<Expense>[] {
const { handleEdit, handleDeleteClick, can } = params;
return [
const columns: ColumnDef<Expense>[] = [
{
accessorKey: 'formatted_created_at',
header: () => <span>Tanggal</span>,
@ -83,7 +83,10 @@ export function createExpenseColumns(
return <span>{createdBy?.user_profile?.full_name ?? '-'}</span>;
},
},
{
];
if (can('expenses.update') || can('expenses.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -114,6 +117,8 @@ export function createExpenseColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -174,58 +174,57 @@ export function createPayrollPeriodColumns(
});
}
columns.push(
{
accessorKey: 'status',
header: () => <span>Status</span>,
cell: ({ row }) => (
<span>{getStatusBadge(row.getValue('status') as string)}</span>
),
},
{
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
className: 'w-[100px] text-center',
headerClassName: 'w-[100px] text-center',
},
cell: ({ row }) => {
const period = row.original;
columns.push({
accessorKey: 'status',
header: () => <span>Status</span>,
cell: ({ row }) => (
<span>{getStatusBadge(row.getValue('status') as string)}</span>
),
});
return (
<RowActions
actions={[
{
label: 'Lihat Detail',
icon: <Eye className="h-4 w-4" />,
href: showUrl(period.id),
},
{
label: 'Tutup Periode',
icon: (
<Lock className="h-4 w-4 text-orange-600" />
),
show:
can('payroll.adjust') &&
period.status === 'open',
onClick: () => handleClose(period),
},
{
label: 'Buka Periode',
icon: (
<Unlock className="h-4 w-4 text-blue-600" />
),
show:
can('payroll.adjust') &&
period.status === 'closed',
onClick: () => handleReopen(period),
},
]}
/>
);
},
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
className: 'w-[100px] text-center',
headerClassName: 'w-[100px] text-center',
},
);
cell: ({ row }) => {
const period = row.original;
return (
<RowActions
actions={[
{
label: 'Lihat Detail',
icon: <Eye className="h-4 w-4" />,
href: showUrl(period.id),
},
{
label: 'Tutup Periode',
icon: (
<Lock className="h-4 w-4 text-orange-600" />
),
show:
can('payroll.adjust') &&
period.status === 'open',
onClick: () => handleClose(period),
},
{
label: 'Buka Periode',
icon: (
<Unlock className="h-4 w-4 text-blue-600" />
),
show:
can('payroll.adjust') &&
period.status === 'closed',
onClick: () => handleReopen(period),
},
]}
/>
);
},
});
return columns;
}

View File

@ -54,7 +54,7 @@ export function createEmployeeColumns(
canViewAll,
} = params;
return [
const columns: ColumnDef<Employee>[] = [
{
accessorKey: 'user_profile.full_name',
id: 'full_name',
@ -154,7 +154,14 @@ export function createEmployeeColumns(
);
},
},
{
];
if (
can('employees.update') ||
can('employees.reset_password') ||
can('employees.delete')
) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -193,6 +200,8 @@ export function createEmployeeColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -64,7 +64,7 @@ export function createLeaveRequestColumns(
const { handleEdit, handleDeleteClick, handleApprove, handleReject, can } =
params;
return [
const columns: ColumnDef<LeaveRequest>[] = [
{
id: 'employee_name',
header: () => <span>Oleh</span>,
@ -112,7 +112,14 @@ export function createLeaveRequestColumns(
<span>{getStatusBadge(row.getValue('status') as string)}</span>
),
},
{
];
if (
can('leave_requests.verify') ||
can('leave_requests.update') ||
can('leave_requests.delete')
) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -167,6 +174,8 @@ export function createLeaveRequestColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -139,25 +139,27 @@ export function CuttingCardRow({
)}
</div>
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('cuttings.update'),
onClick: () => onEdit(cutting),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('cuttings.delete'),
onClick: () => onDelete(cutting),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
{(can('cuttings.update') || can('cuttings.delete')) && (
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('cuttings.update'),
onClick: () => onEdit(cutting),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('cuttings.delete'),
onClick: () => onDelete(cutting),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
)}
</div>
</CardContent>
</Card>

View File

@ -126,25 +126,27 @@ export function PurchaseCardRow({
)}
</div>
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('purchases.update'),
onClick: () => onEdit(purchase),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('purchases.delete'),
onClick: () => onDelete(purchase),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
{(can('purchases.update') || can('purchases.delete')) && (
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('purchases.update'),
onClick: () => onEdit(purchase),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('purchases.delete'),
onClick: () => onDelete(purchase),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
)}
</div>
</CardContent>
</Card>

View File

@ -135,25 +135,27 @@ export function RestockCardRow({
</div>
</div>
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('restocks.update'),
onClick: () => onEdit(restock),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('restocks.delete'),
onClick: () => onDelete(restock),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
{(can('restocks.update') || can('restocks.delete')) && (
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('restocks.update'),
onClick: () => onEdit(restock),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('restocks.delete'),
onClick: () => onDelete(restock),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
)}
</div>
</CardContent>
</Card>

View File

@ -183,57 +183,59 @@ export function TransactionCardRow({
</div>
</div>
<RowActions
actions={[
...(transaction.status === 'pending' && can('orders.update')
? [
{
label: 'Kirim',
icon: <Send className="h-4 w-4" />,
onClick: () => onUpdateStatus(transaction, 'processing'),
},
]
: []),
...((transaction.status === 'pending' || transaction.status === 'processing') && can('orders.update')
? [
{
label: 'Selesai',
icon: <CheckCircle className="h-4 w-4" />,
onClick: () => onUpdateStatus(transaction, 'completed'),
},
{
label: 'Dibatalkan',
icon: <XCircle className="h-4 w-4 text-destructive" />,
onClick: () => onUpdateStatus(transaction, 'cancelled'),
},
]
: []),
...(transaction.status === 'completed' && can('orders.update')
? [
{
label: 'Refund',
icon: <XCircle className="h-4 w-4 text-destructive" />,
onClick: () => onUpdateStatus(transaction, 'refunded'),
},
]
: []),
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('orders.update'),
onClick: () => onEdit(transaction),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('orders.delete'),
onClick: () => onDelete(transaction),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
{(can('orders.update') || can('orders.delete')) && (
<RowActions
actions={[
...(transaction.status === 'pending' && can('orders.update')
? [
{
label: 'Kirim',
icon: <Send className="h-4 w-4" />,
onClick: () => onUpdateStatus(transaction, 'processing'),
},
]
: []),
...((transaction.status === 'pending' || transaction.status === 'processing') && can('orders.update')
? [
{
label: 'Selesai',
icon: <CheckCircle className="h-4 w-4" />,
onClick: () => onUpdateStatus(transaction, 'completed'),
},
{
label: 'Dibatalkan',
icon: <XCircle className="h-4 w-4 text-destructive" />,
onClick: () => onUpdateStatus(transaction, 'cancelled'),
},
]
: []),
...(transaction.status === 'completed' && can('orders.update')
? [
{
label: 'Refund',
icon: <XCircle className="h-4 w-4 text-destructive" />,
onClick: () => onUpdateStatus(transaction, 'refunded'),
},
]
: []),
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('orders.update'),
onClick: () => onEdit(transaction),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('orders.delete'),
onClick: () => onDelete(transaction),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
)}
</div>
</CardContent>
</Card>

View File

@ -18,7 +18,7 @@ export function createCategoryColumns(
): ColumnDef<Category>[] {
const { handleEdit, handleDeleteClick, can } = params;
return [
const columns: ColumnDef<Category>[] = [
{
accessorKey: 'name',
header: () => <span>Nama</span>,
@ -28,7 +28,10 @@ export function createCategoryColumns(
</span>
),
},
{
];
if (can('categories.update') || can('categories.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -59,6 +62,8 @@ export function createCategoryColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -20,7 +20,7 @@ export function createCustomerColumns(
): ColumnDef<Customer>[] {
const { handleEdit, handleDeleteClick, can } = params;
return [
const columns: ColumnDef<Customer>[] = [
{
accessorKey: 'name',
header: () => <span>Nama</span>,
@ -46,7 +46,10 @@ export function createCustomerColumns(
</span>
),
},
{
];
if (can('customers.update') || can('customers.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -77,6 +80,8 @@ export function createCustomerColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -90,7 +90,7 @@ export function createProductColumns(
can,
} = params;
return [
const columns: ColumnDef<Product>[] = [
{
id: 'expand',
header: '',
@ -338,7 +338,10 @@ export function createProductColumns(
);
},
},
{
];
if (can('products.update') || can('products.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -369,6 +372,8 @@ export function createProductColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -144,25 +144,27 @@ export function ProductCardRow({
</div>
</div>
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('products.update'),
onClick: () => onEdit(product),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('products.delete'),
onClick: () => onDelete(product),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
{(can('products.update') || can('products.delete')) && (
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('products.update'),
onClick: () => onEdit(product),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('products.delete'),
onClick: () => onDelete(product),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
)}
</div>
</CardContent>
</Card>

View File

@ -34,6 +34,8 @@ export function VariantSubRow({
variant: ProductVariant;
} | null>(null);
const canAnyAction = can('products.transfer_stock') || can('products.view_stock_mutations') || can('products.update') || can('products.delete');
return (
<div className="overflow-x-auto">
<Table>
@ -52,16 +54,18 @@ export function VariantSubRow({
</TableHead>
<TableHead className="text-center">Stok Ecer</TableHead>
<TableHead>Harga</TableHead>
<TableHead className="w-[120px] text-center">
Aksi
</TableHead>
{canAnyAction && (
<TableHead className="w-[120px] text-center">
Aksi
</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{variants.length === 0 ? (
<TableRow>
<TableCell
colSpan={8}
colSpan={canAnyAction ? 8 : 7}
className="text-center text-muted-foreground"
>
Tidak ada varian.
@ -116,63 +120,65 @@ export function VariantSubRow({
'-'
)}
</TableCell>
<TableCell>
<RowActions
actions={[
{
label: 'Transfer Stok',
icon: (
<ArrowRightLeft className="h-4 w-4" />
),
show: can('products.transfer_stock'),
onClick: () =>
setTransferVariant({
product,
variant,
}),
},
{
label: 'Mutasi Stok',
icon: (
<ScrollText className="h-4 w-4" />
),
show: can('products.view_stock_mutations'),
onClick: () => {
router.visit(
stockMutations.url({
product: product.id,
variant: variant.id,
{canAnyAction && (
<TableCell>
<RowActions
actions={[
{
label: 'Transfer Stok',
icon: (
<ArrowRightLeft className="h-4 w-4" />
),
show: can('products.transfer_stock'),
onClick: () =>
setTransferVariant({
product,
variant,
}),
);
},
},
{
label: 'Edit',
icon: (
<Pencil className="h-4 w-4" />
),
show: can('products.update'),
onClick: () =>
onEditVariant(
product,
variant,
{
label: 'Mutasi Stok',
icon: (
<ScrollText className="h-4 w-4" />
),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('products.delete'),
onClick: () =>
onDeleteVariantClick(
product,
variant,
show: can('products.view_stock_mutations'),
onClick: () => {
router.visit(
stockMutations.url({
product: product.id,
variant: variant.id,
}),
);
},
},
{
label: 'Edit',
icon: (
<Pencil className="h-4 w-4" />
),
},
]}
/>
</TableCell>
show: can('products.update'),
onClick: () =>
onEditVariant(
product,
variant,
),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('products.delete'),
onClick: () =>
onDeleteVariantClick(
product,
variant,
),
},
]}
/>
</TableCell>
)}
</TableRow>
))
)}

View File

@ -94,25 +94,27 @@ export function RawMaterialCardRow({
</div>
</div>
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('raw_materials.update'),
onClick: () => onEdit(rawMaterial),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('raw_materials.delete'),
onClick: () => onDelete(rawMaterial),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
{(can('raw_materials.update') || can('raw_materials.delete')) && (
<RowActions
actions={[
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,
show: can('raw_materials.update'),
onClick: () => onEdit(rawMaterial),
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('raw_materials.delete'),
onClick: () => onDelete(rawMaterial),
},
]}
wrapperClassName="flex shrink-0 items-center gap-1"
/>
)}
</div>
</CardContent>
</Card>

View File

@ -47,6 +47,8 @@ export function RawMaterialVariantSubRow({
);
}
const canAnyAction = can('raw_materials.update') || can('raw_materials.delete');
return (
<div className="overflow-x-auto">
<Table>
@ -59,16 +61,18 @@ export function RawMaterialVariantSubRow({
<TableHead>Nama Varian</TableHead>
<TableHead>Harga</TableHead>
<TableHead className="text-center">Stok</TableHead>
<TableHead className="w-[100px] text-center">
Aksi
</TableHead>
{canAnyAction && (
<TableHead className="w-[100px] text-center">
Aksi
</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{variants.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={canAnyAction ? 6 : 5}
className="text-center text-muted-foreground"
>
Tidak ada varian.
@ -101,37 +105,39 @@ export function RawMaterialVariantSubRow({
<TableCell className="text-center">
{formatNumber(variant.stock)}
</TableCell>
<TableCell>
<RowActions
actions={[
{
label: 'Edit',
icon: (
<Pencil className="h-4 w-4" />
),
show: can('raw_materials.update'),
onClick: () => {
router.visit(
variantEdit.url({
rawMaterial:
rawMaterial.id,
variant: variant.id,
}),
);
{canAnyAction && (
<TableCell>
<RowActions
actions={[
{
label: 'Edit',
icon: (
<Pencil className="h-4 w-4" />
),
show: can('raw_materials.update'),
onClick: () => {
router.visit(
variantEdit.url({
rawMaterial:
rawMaterial.id,
variant: variant.id,
}),
);
},
},
},
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('raw_materials.delete'),
onClick: () =>
setDeletingVariant(variant),
},
]}
/>
</TableCell>
{
label: 'Hapus',
icon: (
<Trash2 className="h-4 w-4 text-destructive" />
),
show: can('raw_materials.delete'),
onClick: () =>
setDeletingVariant(variant),
},
]}
/>
</TableCell>
)}
</TableRow>
))
)}

View File

@ -20,7 +20,7 @@ export function createSupplierColumns(
): ColumnDef<Supplier>[] {
const { handleEdit, handleDeleteClick, can } = params;
return [
const columns: ColumnDef<Supplier>[] = [
{
accessorKey: 'name',
header: () => <span>Nama</span>,
@ -46,7 +46,10 @@ export function createSupplierColumns(
</span>
),
},
{
];
if (can('suppliers.update') || can('suppliers.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -77,6 +80,8 @@ export function createSupplierColumns(
/>
);
},
},
];
});
}
return columns;
}

View File

@ -19,7 +19,7 @@ export function createRoleColumns(
): ColumnDef<Role>[] {
const { handleEdit, handleDeleteClick, can } = params;
return [
const columns: ColumnDef<Role>[] = [
{
accessorKey: 'name',
header: () => <span>Nama Role</span>,
@ -44,7 +44,10 @@ export function createRoleColumns(
</span>
),
},
{
];
if (can('roles.update') || can('roles.delete')) {
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
meta: {
@ -75,6 +78,8 @@ export function createRoleColumns(
/>
);
},
},
];
});
}
return columns;
}