feat(FE-331): implement permission guard in project flock

This commit is contained in:
ValdiANS
2025-12-26 21:08:00 +07:00
parent 4be719b9d8
commit 9e0d3e2bbf
5 changed files with 242 additions and 173 deletions
+52 -24
View File
@@ -5,6 +5,8 @@ import Tooltip from '@/components/Tooltip';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useAuth } from '@/services/hooks/useAuth';
type FloatingActionsButtonProps = { type FloatingActionsButtonProps = {
actions: { actions: {
action: 'DETAIL' | 'EDIT' | 'DELETE'; action: 'DETAIL' | 'EDIT' | 'DELETE';
@@ -13,6 +15,7 @@ type FloatingActionsButtonProps = {
onClick?: () => void; onClick?: () => void;
hidden?: boolean; hidden?: boolean;
disabled?: boolean; disabled?: boolean;
permissions?: string | string[];
}[]; }[];
approvals: { approvals: {
action: 'APPROVED' | 'REJECTED'; action: 'APPROVED' | 'REJECTED';
@@ -20,6 +23,7 @@ type FloatingActionsButtonProps = {
label?: string; label?: string;
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
permissions?: string | string[];
}[]; }[];
selectedRowIds: number[]; selectedRowIds: number[];
onClose: () => void; onClose: () => void;
@@ -31,6 +35,7 @@ const FloatingActionsButton = ({
selectedRowIds, selectedRowIds,
onClose, onClose,
}: FloatingActionsButtonProps) => { }: FloatingActionsButtonProps) => {
const { permissionCheck } = useAuth();
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB // Jika tidak ada baris yang dipilih, jangan tampilkan FAB
const positionStyles = const positionStyles =
selectedRowIds.length > 0 selectedRowIds.length > 0
@@ -71,7 +76,18 @@ const FloatingActionsButton = ({
<div className='flex gap-4 items-center'> <div className='flex gap-4 items-center'>
{/* Render Aksi dari props.actions */} {/* Render Aksi dari props.actions */}
{actions {actions
.filter((action) => !action.hidden) .filter((action) => {
if (action.hidden) return false;
if (action.permissions) {
if (typeof action.permissions === 'string') {
return permissionCheck(action.permissions);
}
return action.permissions.some((permission) =>
permissionCheck(permission)
);
}
return true;
})
.map((action, index) => { .map((action, index) => {
return ( return (
<Button <Button
@@ -111,29 +127,41 @@ const FloatingActionsButton = ({
{/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */} {/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
<div className={`grid grid-cols-${approvals.length} gap-3`}> <div className={`grid grid-cols-${approvals.length} gap-3`}>
{approvals.map((approval, index) => ( {approvals
<Button .filter((approval) => {
key={index} if (approval.permissions) {
onClick={approval.onClick} if (typeof approval.permissions === 'string') {
className={cn( return permissionCheck(approval.permissions);
'btn btn-lg w-full', }
'bg-white/20 border-white/30', return approval.permissions.some((permission) =>
'text-white/50 font-semibold flex items-center gap-2 rounded-lg transition-all duration-200', permissionCheck(permission)
approval.disabled );
? 'cursor-not-allowed' }
: 'hover:text-white/100 hover:bg-white/40 hover:border-white/50' return true;
)} })
disabled={approval.disabled} .map((approval, index) => (
> <Button
<Icon key={index}
icon={approval.icon} onClick={approval.onClick}
width={20} className={cn(
height={20} 'btn btn-lg w-full',
className={`text-${getApprovalColor(approval.action)}`} 'bg-white/20 border-white/30',
/> 'text-white/50 font-semibold flex items-center gap-2 rounded-lg transition-all duration-200',
{approval.label || approval.action} approval.disabled
</Button> ? 'cursor-not-allowed'
))} : 'hover:text-white/100 hover:bg-white/40 hover:border-white/50'
)}
disabled={approval.disabled}
>
<Icon
icon={approval.icon}
width={20}
height={20}
className={`text-${getApprovalColor(approval.action)}`}
/>
{approval.label || approval.action}
</Button>
))}
</div> </div>
</div> </div>
</div> </div>
@@ -25,6 +25,8 @@ import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import useSWR from 'swr'; import useSWR from 'swr';
import RequirePermission from '@/components/helper/RequirePermission';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', type = 'dropdown',
props, props,
@@ -46,50 +48,58 @@ const RowOptionsMenu = ({
)} )}
> >
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<Button <RequirePermission permissions='lti.production.project_flocks.detail'>
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
{props.row.original.approval.step_name === 'Aktif' && (
<Button <Button
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`} href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='success' color='primary'
className='justify-start text-sm' className='justify-start text-sm'
> >
<Icon icon='mdi:home-import-outline' width={16} height={16} /> <Icon icon='mdi:eye-outline' width={16} height={16} />
Chickin Detail
</Button> </Button>
</RequirePermission>
{props.row.original.approval.step_name === 'Aktif' && (
<RequirePermission permissions='lti.production.chickins.create'>
<Button
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:home-import-outline' width={16} height={16} />
Chickin
</Button>
</RequirePermission>
)} )}
{props.row.original.approval.step_name === 'Pengajuan' && ( {props.row.original.approval.step_name === 'Pengajuan' && (
<Button <RequirePermission permissions='lti.production.project_flocks.update'>
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`} <Button
variant='ghost' href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
color='warning' variant='ghost'
className='justify-start text-sm' color='warning'
> className='justify-start text-sm'
<Icon icon='mdi:pencil-outline' width={16} height={16} /> >
Edit <Icon icon='mdi:pencil-outline' width={16} height={16} />
</Button> Edit
</Button>
</RequirePermission>
)} )}
<Button <RequirePermission permissions='lti.production.project_flocks.delete'>
onClick={deleteClickHandler} <Button
variant='ghost' onClick={deleteClickHandler}
color='error' variant='ghost'
className='text-error hover:text-inherit justify-start text-sm' color='error'
> className='text-error hover:text-inherit justify-start text-sm'
<Icon >
icon='material-symbols:delete-outline-rounded' <Icon
width={16} icon='material-symbols:delete-outline-rounded'
height={16} width={16}
/> height={16}
Delete />
</Button> Delete
</Button>
</RequirePermission>
</div> </div>
</div> </div>
); );
@@ -287,14 +297,16 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<div className='flex flex-col gap-2 mb-4'> <div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col justify-between items-end gap-2'> <div className='w-full flex flex-col justify-between items-end gap-2'>
<div className='flex flex-col sm:flex-row gap-3 w-full'> <div className='flex flex-col sm:flex-row gap-3 w-full'>
<Button <RequirePermission permissions='lti.production.project_flocks.create'>
color='primary' <Button
className='w-full sm:w-fit' color='primary'
href='/production/project-flock/add' className='w-full sm:w-fit'
> href='/production/project-flock/add'
<Icon icon='ic:round-plus' width={24} height={24} /> >
Tambah <Icon icon='ic:round-plus' width={24} height={24} />
</Button> Tambah
</Button>
</RequirePermission>
{/* <Button {/* <Button
variant='outline' variant='outline'
color='success' color='success'
@@ -630,6 +642,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
); );
setRowSelection({}); setRowSelection({});
}, },
permissions: 'lti.production.project_flocks.detail',
}, },
{ {
action: 'DELETE', action: 'DELETE',
@@ -639,6 +652,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
onClick: () => { onClick: () => {
deleteModal.openModal(); deleteModal.openModal();
}, },
permissions: 'lti.production.project_flocks.delete',
}, },
]} ]}
approvals={[ approvals={[
@@ -651,6 +665,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
confirmModal.openModal(); confirmModal.openModal();
}, },
disabled: !canApprove, disabled: !canApprove,
permissions: 'lti.production.project_flocks.approve',
}, },
{ {
icon: 'mdi:times', icon: 'mdi:times',
@@ -660,6 +675,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
setApprovalAction('REJECTED'); setApprovalAction('REJECTED');
confirmModal.openModal(); confirmModal.openModal();
}, },
permissions: 'lti.production.project_flocks.approve',
}, },
]} ]}
selectedRowIds={selectedRowIds} selectedRowIds={selectedRowIds}
@@ -22,6 +22,7 @@ import { useEffect, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { FormHeader } from '@/components/helper/form/FormHeader'; import { FormHeader } from '@/components/helper/form/FormHeader';
import Link from 'next/link'; import Link from 'next/link';
import RequirePermission from '@/components/helper/RequirePermission';
const ProjectFlockChickinDetail = ({ const ProjectFlockChickinDetail = ({
projectFlockId, projectFlockId,
@@ -484,21 +485,23 @@ const ProjectFlockChickinDetail = ({
{kandang.kandang.name} {kandang.kandang.name}
</span> </span>
</div> </div>
<Button <RequirePermission permissions='lti.production.chickins.create'>
variant='outline' <Button
className='py-1 text-sm' variant='outline'
onClick={() => { className='py-1 text-sm'
handleChickinClick(kandang); onClick={() => {
}} handleChickinClick(kandang);
disabled={projectFlock?.approval?.step_number === 1} }}
> disabled={projectFlock?.approval?.step_number === 1}
Chick In{' '} >
<Icon Chick In{' '}
icon='mdi:arrow-top-right-thin' <Icon
width={11} icon='mdi:arrow-top-right-thin'
height={11} width={11}
/> height={11}
</Button> />
</Button>
</RequirePermission>
</div> </div>
))} ))}
</div> </div>
@@ -29,6 +29,7 @@ import {
} from '@/config/approval-line'; } from '@/config/approval-line';
import useSWR from 'swr'; import useSWR from 'swr';
import { ProjectFlockKandangApi } from '@/services/api/production'; import { ProjectFlockKandangApi } from '@/services/api/production';
import RequirePermission from '@/components/helper/RequirePermission';
const ProjectFlockDetail = ({ const ProjectFlockDetail = ({
projectFlock, projectFlock,
@@ -110,27 +111,31 @@ const ProjectFlockDetail = ({
leftIconHref='/production/project-flock' leftIconHref='/production/project-flock'
subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`} subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`}
> >
<Link <RequirePermission permissions='lti.production.project_flocks.update'>
href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`} <Link
className='p-0' href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`}
> className='p-0'
<Tooltip content='Edit' position='bottom'> >
<Button variant='link' className='p-0 text-neutral'> <Tooltip content='Edit' position='bottom'>
<Icon icon='mdi:square-edit-outline' width={20} height={20} /> <Button variant='link' className='p-0 text-neutral'>
</Button> <Icon icon='mdi:square-edit-outline' width={20} height={20} />
</Tooltip> </Button>
</Link> </Tooltip>
<Button </Link>
variant='link' </RequirePermission>
className='p-0 text-error' <RequirePermission permissions='lti.production.project_flocks.delete'>
onClick={() => { <Button
deleteModal.openModal(); variant='link'
}} className='p-0 text-error'
> onClick={() => {
<Tooltip content='Hapus' position='bottom'> deleteModal.openModal();
<Icon icon='mdi:trash-can-outline' width={20} height={20} /> }}
</Tooltip> >
</Button> <Tooltip content='Hapus' position='bottom'>
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
</Tooltip>
</Button>
</RequirePermission>
</DrawerHeader> </DrawerHeader>
{/* Informasi Umum */} {/* Informasi Umum */}
@@ -418,38 +423,42 @@ const ProjectFlockDetail = ({
</RadioGroup> </RadioGroup>
</Card> </Card>
<div className='grid grid-cols-4 gap-3'> <div className='grid grid-cols-4 gap-3'>
<Link <RequirePermission permissions='lti.production.chickins.create'>
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`} <Link
className='m-0 p-0' href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`}
> className='m-0 p-0'
<Button
className='w-full px-2 py-1 text-sm'
variant='outline'
color='success'
disabled={
!selectedKandangId ||
projectFlock?.approval?.step_number == 1
}
> >
Chickin <Icon icon='mdi:checkbox-marked-outline' /> <Button
</Button> className='w-full px-2 py-1 text-sm'
</Link> variant='outline'
<Link color='success'
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`} disabled={
className='m-0 p-0' !selectedKandangId ||
> projectFlock?.approval?.step_number == 1
<Button }
className='w-full px-2 py-1 text-sm' >
variant='outline' Chickin <Icon icon='mdi:checkbox-marked-outline' />
color='error' </Button>
disabled={ </Link>
!selectedKandangId || </RequirePermission>
projectFlock?.approval?.step_number == 1 <RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
} <Link
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
className='m-0 p-0'
> >
Close <Icon icon='mdi:checkbox-marked-circle-outline' /> <Button
</Button> className='w-full px-2 py-1 text-sm'
</Link> variant='outline'
color='error'
disabled={
!selectedKandangId ||
projectFlock?.approval?.step_number == 1
}
>
Close <Icon icon='mdi:checkbox-marked-circle-outline' />
</Button>
</Link>
</RequirePermission>
</div> </div>
</div> </div>
</div> </div>
@@ -47,6 +47,7 @@ import Card from '@/components/Card';
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable'; import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
import { Nonstock } from '@/types/api/master-data/nonstock'; import { Nonstock } from '@/types/api/master-data/nonstock';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import RequirePermission from '@/components/helper/RequirePermission';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
interface ProjectFlockFormProps { interface ProjectFlockFormProps {
@@ -734,36 +735,40 @@ const ProjectFlockForm = ({
)} )}
{formType == 'detail' && ( {formType == 'detail' && (
<div className='w-full flex flex-col sm:flex-row gap-2 py-4'> <div className='w-full flex flex-col sm:flex-row gap-2 py-4'>
<Button <RequirePermission permissions='lti.production.project_flocks.approve'>
variant='outline' <Button
color='success' variant='outline'
onClick={() => { color='success'
if (initialValues?.id) { onClick={() => {
setApprovalAction('APPROVED'); if (initialValues?.id) {
confirmModal.openModal(); setApprovalAction('APPROVED');
} confirmModal.openModal();
}} }
disabled={!initialValues?.id || isApprovedDisabled} }}
className='w-full sm:w-fit' disabled={!initialValues?.id || isApprovedDisabled}
> className='w-full sm:w-fit'
<Icon icon='material-symbols:check' width={24} height={24} /> >
Approve <Icon icon='material-symbols:check' width={24} height={24} />
</Button> Approve
<Button </Button>
variant='outline' </RequirePermission>
color='error' <RequirePermission permissions='lti.production.project_flocks.approve'>
onClick={() => { <Button
if (initialValues?.id) { variant='outline'
setApprovalAction('REJECTED'); color='error'
confirmModal.openModal(); onClick={() => {
} if (initialValues?.id) {
}} setApprovalAction('REJECTED');
disabled={!initialValues?.id || isRejectedDisabled} confirmModal.openModal();
className='w-full sm:w-fit' }
> }}
<Icon icon='mdi:times' width={24} height={24} /> disabled={!initialValues?.id || isRejectedDisabled}
Reject className='w-full sm:w-fit'
</Button> >
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button>
</RequirePermission>
</div> </div>
)} )}
<form <form
@@ -1127,16 +1132,24 @@ const ProjectFlockForm = ({
</div> </div>
</div> */} </div> */}
{formType !== 'detail' && ( {formType !== 'detail' && (
<Button <RequirePermission
type='submit' permissions={
color='primary' formType == 'add'
isLoading={formik.isSubmitting} ? 'lti.production.project_flocks.create'
disabled={!formik.isValid || formik.isSubmitting} : 'lti.production.project_flocks.update'
className='px-4 w-full' }
> >
<Icon icon='mdi:plus' width={24} height={24} /> <Button
{formType == 'add' ? 'Add Flock' : 'Update Flock'} type='submit'
</Button> color='primary'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting}
className='px-4 w-full'
>
<Icon icon='mdi:plus' width={24} height={24} />
{formType == 'add' ? 'Add Flock' : 'Update Flock'}
</Button>
</RequirePermission>
)} )}
</div> </div>
</form> </form>