feat(FE-331): implement permission guard in recording

This commit is contained in:
ValdiANS
2025-12-27 14:34:59 +07:00
parent 9e0d3e2bbf
commit 507c4005af
2 changed files with 177 additions and 143 deletions
@@ -6,6 +6,7 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { SortingState, CellContext } from '@tanstack/react-table'; import { SortingState, CellContext } from '@tanstack/react-table';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import RequirePermission from '@/components/helper/RequirePermission';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import Button from '@/components/Button'; import Button from '@/components/Button';
@@ -59,60 +60,70 @@ const RowOptionsMenu = ({
return ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
<Button <RequirePermission permissions='lti.production.recording.detail'>
href={`/production/recording/detail/?recordingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
{!isApproved && !isRejected && (
<Button <Button
onClick={approveClickHandler} href={`/production/recording/detail/?recordingId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='success' color='primary'
className='justify-start text-sm' className='justify-start text-sm'
> >
<Icon icon='material-symbols:check' width={16} height={16} /> <Icon icon='mdi:eye-outline' width={16} height={16} />
Approve Detail
</Button> </Button>
</RequirePermission>
<RequirePermission permissions='lti.production.recording.update'>
<Button
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
{!isApproved && !isRejected && (
<RequirePermission permissions='lti.production.recording.approve'>
<Button
onClick={approveClickHandler}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='material-symbols:check' width={16} height={16} />
Approve
</Button>
</RequirePermission>
)} )}
{!isApproved && !isRejected && ( {!isApproved && !isRejected && (
<RequirePermission permissions='lti.production.recording.approve'>
<Button
onClick={rejectClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm'
>
<Icon icon='material-symbols:close' width={16} height={16} />
Reject
</Button>
</RequirePermission>
)}
<RequirePermission permissions='lti.production.recording.delete'>
<Button <Button
onClick={rejectClickHandler} onClick={deleteClickHandler}
variant='ghost' variant='ghost'
color='error' color='error'
className='justify-start text-sm' className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
> >
<Icon icon='material-symbols:close' width={16} height={16} /> <Icon
Reject icon='mdi:delete-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button> </Button>
)} </RequirePermission>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='mdi:delete-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RowOptionsMenuWrapper> </RowOptionsMenuWrapper>
); );
}; };
@@ -514,49 +525,63 @@ const RecordingTable = () => {
<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 xl:flex-row justify-between items-end xl:items-center gap-2'> <div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'> <div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
<Button <RequirePermission permissions='lti.production.recording.create'>
href='/production/recording/add' <Button
variant='outline' href='/production/recording/add'
color='primary' variant='outline'
className='w-full sm:w-fit' color='primary'
> className='w-full sm:w-fit'
<Icon icon='ic:round-plus' width={24} height={24} /> >
Tambah <Icon icon='ic:round-plus' width={24} height={24} />
</Button> Tambah
</Button>
</RequirePermission>
{selectedRowIds.length > 0 && ( {selectedRowIds.length > 0 && (
<> <>
<Button <RequirePermission permissions='lti.production.recording.approve'>
variant='outline' <Button
color='success' variant='outline'
onClick={() => { color='success'
setApprovalNotes(''); onClick={() => {
approveModal.openModal(); setApprovalNotes('');
}} approveModal.openModal();
disabled={ }}
selectedRowIds.length === 0 || eligibleRowIds.length === 0 disabled={
} selectedRowIds.length === 0 || eligibleRowIds.length === 0
className='w-full sm:w-fit' }
> className='w-full sm:w-fit'
<Icon icon='material-symbols:check' width={24} height={24} /> >
Approve <Icon
</Button> icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
</RequirePermission>
<Button <RequirePermission permissions='lti.production.recording.approve'>
variant='outline' <Button
color='error' variant='outline'
onClick={() => { color='error'
setApprovalNotes(''); onClick={() => {
rejectModal.openModal(); setApprovalNotes('');
}} rejectModal.openModal();
disabled={ }}
selectedRowIds.length === 0 || eligibleRowIds.length === 0 disabled={
} selectedRowIds.length === 0 || eligibleRowIds.length === 0
className='w-full sm:w-fit' }
> className='w-full sm:w-fit'
<Icon icon='material-symbols:close' width={24} height={24} /> >
Reject <Icon
</Button> icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
</RequirePermission>
</> </>
)} )}
</div> </div>
@@ -8,6 +8,7 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import RequirePermission from '@/components/helper/RequirePermission';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
@@ -1492,41 +1493,45 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
!isRecordingApproved(initialValues) && !isRecordingApproved(initialValues) &&
!isRecordingRejected(initialValues) && ( !isRecordingRejected(initialValues) && (
<div className='flex flex-row gap-2'> <div className='flex flex-row gap-2'>
<Button <RequirePermission permissions='lti.production.recording.approve'>
variant='outline' <Button
color='success' variant='outline'
onClick={() => { color='success'
setApprovalNotes(''); onClick={() => {
approveModal.openModal(); setApprovalNotes('');
}} approveModal.openModal();
isLoading={isApproveLoading} }}
className='w-full sm:w-fit' isLoading={isApproveLoading}
> className='w-full sm:w-fit'
<Icon >
icon='material-symbols:check' <Icon
width={24} icon='material-symbols:check'
height={24} width={24}
/> height={24}
Approve />
</Button> Approve
</Button>
</RequirePermission>
<Button <RequirePermission permissions='lti.production.recording.approve'>
variant='outline' <Button
color='error' variant='outline'
onClick={() => { color='error'
setApprovalNotes(''); onClick={() => {
rejectModal.openModal(); setApprovalNotes('');
}} rejectModal.openModal();
isLoading={isRejectLoading} }}
className='w-full sm:w-fit' isLoading={isRejectLoading}
> className='w-full sm:w-fit'
<Icon >
icon='material-symbols:close' <Icon
width={24} icon='material-symbols:close'
height={24} width={24}
/> height={24}
Reject />
</Button> Reject
</Button>
</RequirePermission>
</div> </div>
)} )}
</div> </div>
@@ -2696,36 +2701,40 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Left side - Detail & Edit actions */} {/* Left side - Detail & Edit actions */}
<div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'> <div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'>
{type === 'detail' && deleteRecordingClickHandler && ( {type === 'detail' && deleteRecordingClickHandler && (
<Button <RequirePermission permissions='lti.production.recording.delete'>
type='button' <Button
color='error' type='button'
onClick={deleteRecordingClickHandler} color='error'
className='px-4' onClick={deleteRecordingClickHandler}
> className='px-4'
<Icon >
icon='material-symbols:delete-outline-rounded' <Icon
width={24} icon='material-symbols:delete-outline-rounded'
height={24} width={24}
className='justify-start text-sm' height={24}
/> className='justify-start text-sm'
Delete />
</Button> Delete
</Button>
</RequirePermission>
)} )}
{type === 'detail' && initialValues && ( {type === 'detail' && initialValues && (
<Button <RequirePermission permissions='lti.production.recording.update'>
type='button' <Button
color='warning' type='button'
href={`/production/recording/detail/edit/?recordingId=${initialValues.id}`} color='warning'
className='px-4' href={`/production/recording/detail/edit/?recordingId=${initialValues.id}`}
> className='px-4'
<Icon >
icon='material-symbols:edit-outline' <Icon
width={24} icon='material-symbols:edit-outline'
height={24} width={24}
className='justify-start text-sm' height={24}
/> className='justify-start text-sm'
Edit />
</Button> Edit
</Button>
</RequirePermission>
)} )}
</div> </div>
{/* Right side actions */} {/* Right side actions */}