feat(FE-170,175): enhance RecordingForm and RecordingTable with approval logic and UI improvements

This commit is contained in:
rstubryan
2025-11-06 17:12:30 +07:00
parent ffa11fa20a
commit d8b076d105
2 changed files with 131 additions and 55 deletions
@@ -51,6 +51,16 @@ const RowOptionsMenu = ({
const isLayingCategory = const isLayingCategory =
props.row.original.project_flock_category === 'LAYING'; props.row.original.project_flock_category === 'LAYING';
const isRecordingApproved = (recording: Recording) => {
return (
recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui' &&
recording.approval?.step_number === 3
);
};
const isApproved = isRecordingApproved(props.row.original);
return ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
<Button <Button
@@ -82,24 +92,28 @@ const RowOptionsMenu = ({
Grading Grading
</Button> </Button>
)} )}
<Button {!isApproved && (
onClick={approveClickHandler} <Button
variant='ghost' onClick={approveClickHandler}
color='success' variant='ghost'
className='justify-start text-sm' color='success'
> className='justify-start text-sm'
<Icon icon='material-symbols:check' width={16} height={16} /> >
Approve <Icon icon='material-symbols:check' width={16} height={16} />
</Button> Approve
<Button </Button>
onClick={rejectClickHandler} )}
variant='ghost' {!isApproved && (
color='error' <Button
className='justify-start text-sm' onClick={rejectClickHandler}
> variant='ghost'
<Icon icon='material-symbols:close' width={16} height={16} /> color='error'
Reject className='justify-start text-sm'
</Button> >
<Icon icon='material-symbols:close' width={16} height={16} />
Reject
</Button>
)}
<Button <Button
onClick={deleteClickHandler} onClick={deleteClickHandler}
variant='ghost' variant='ghost'
@@ -409,6 +423,14 @@ const RecordingTable = () => {
isLoadingOptions: isLoadingKandang, isLoadingOptions: isLoadingKandang,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name'); } = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const isRecordingApproved = useCallback((recording: Recording) => {
return (
recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui' &&
recording.approval?.step_number === 3
);
}, []);
const searchChangeHandler = useCallback( const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value); updateFilter('search', e.target.value);
@@ -440,14 +462,22 @@ const RecordingTable = () => {
const approveHandler = async () => { const approveHandler = async () => {
setIsApproveLoading(true); setIsApproveLoading(true);
if (eligibleRowIds.length === 0) {
toast.error(
'Tidak ada recording yang bisa disetujui (sudah disetujui sebelumnya)'
);
setIsApproveLoading(false);
return;
}
const approveResponse = await RecordingApi.approve( const approveResponse = await RecordingApi.approve(
selectedRowIds, eligibleRowIds,
approvalNotes approvalNotes
); );
if (isResponseSuccess(approveResponse)) { if (isResponseSuccess(approveResponse)) {
toast.success( toast.success(
`Berhasil approve ${selectedRowIds.length} data recording!` `Berhasil approve ${eligibleRowIds.length} data recording!`
); );
approveModal.closeModal(); approveModal.closeModal();
refreshRecordings(); refreshRecordings();
@@ -465,13 +495,21 @@ const RecordingTable = () => {
const rejectHandler = async () => { const rejectHandler = async () => {
setIsRejectLoading(true); setIsRejectLoading(true);
if (eligibleRowIds.length === 0) {
toast.error(
'Tidak ada recording yang bisa ditolak (sudah disetujui sebelumnya)'
);
setIsRejectLoading(false);
return;
}
const rejectResponse = await RecordingApi.reject( const rejectResponse = await RecordingApi.reject(
selectedRowIds, eligibleRowIds,
approvalNotes approvalNotes
); );
if (isResponseSuccess(rejectResponse)) { if (isResponseSuccess(rejectResponse)) {
toast.success(`Berhasil reject ${selectedRowIds.length} data recording!`); toast.success(`Berhasil reject ${eligibleRowIds.length} data recording!`);
rejectModal.closeModal(); rejectModal.closeModal();
refreshRecordings(); refreshRecordings();
setApprovalNotes(''); setApprovalNotes('');
@@ -485,6 +523,15 @@ const RecordingTable = () => {
setIsRejectLoading(false); setIsRejectLoading(false);
}; };
// Filter out already approved recordings for bulk actions
const eligibleRowIds = useMemo(() => {
if (!isResponseSuccess(recordings) || !recordings.data) return [];
return selectedRowIds.filter((id) => {
const recording = recordings.data.find((r) => r.id === id);
return recording && !isRecordingApproved(recording);
});
}, [selectedRowIds, recordings, isRecordingApproved]);
return ( return (
<div className='w-full p-0 sm:p-4'> <div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'> <div className='flex flex-col gap-2 mb-4'>
@@ -647,17 +694,21 @@ const RecordingTable = () => {
/> />
</div> </div>
), ),
cell: ({ row }) => ( cell: ({ row }) => {
<div> const isApproved = isRecordingApproved(row.original);
<CheckboxInput return (
name='row' <div>
checked={row.getIsSelected()} <CheckboxInput
disabled={!row.getCanSelect()} name='row'
indeterminate={row.getIsSomeSelected()} checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()} disabled={!row.getCanSelect() || isApproved}
/> indeterminate={row.getIsSomeSelected()}
</div> onChange={row.getToggleSelectedHandler()}
), title={isApproved ? 'Recording sudah disetujui' : ''}
/>
</div>
);
},
}, },
{ {
header: '#', header: '#',
@@ -893,7 +944,7 @@ const RecordingTable = () => {
<ConfirmationModal <ConfirmationModal
ref={approveModal.ref} ref={approveModal.ref}
type='success' type='success'
text={`Apakah anda yakin ingin approve data recording ini (${selectedRowIds.length} data)?`} text={`Apakah anda yakin ingin approve data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
onClick: () => setApprovalNotes(''), onClick: () => setApprovalNotes(''),
@@ -917,7 +968,7 @@ const RecordingTable = () => {
<ConfirmationModal <ConfirmationModal
ref={rejectModal.ref} ref={rejectModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin reject data recording ini (${selectedRowIds.length} data)?`} text={`Apakah anda yakin ingin reject data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
onClick: () => setApprovalNotes(''), onClick: () => setApprovalNotes(''),
@@ -2405,9 +2405,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Action buttons */} {/* Action buttons */}
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
{/* Left side - Detail & Edit actions */}
{type !== 'add' && ( {type !== 'add' && (
<div className='flex flex-row justify-start gap-2'> <div className='flex flex-row justify-start gap-2'>
{deleteRecordingClickHandler && ( {type === 'detail' && deleteRecordingClickHandler && (
<Button <Button
type='button' type='button'
color='error' color='error'
@@ -2423,7 +2424,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Delete Delete
</Button> </Button>
)} )}
{type !== 'edit' && initialValues && ( {type === 'detail' && initialValues && (
<Button <Button
type='button' type='button'
color='warning' color='warning'
@@ -2442,11 +2443,50 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
)} )}
<div className='flex flex-row justify-end gap-2'> <div className='flex flex-row justify-end gap-2'>
{type !== 'detail' && ( {type === 'detail' && isLayingCategory && (
<Button
type='button'
color='primary'
onClick={() => {
router.push(
`/production/recording/grading/add?recording_id=${initialValues?.id}`
);
}}
>
<Icon icon='material-symbols:egg' width={24} height={24} />
Lanjut ke Grading
</Button>
)}
{type === 'edit' && (
<>
<Button
type='button'
color='error'
onClick={() => router.push('/production/recording')}
className='px-4'
>
Cancel
</Button>
<Button
type='submit'
color='primary'
className='px-4'
isLoading={formik.isSubmitting}
disabled={
hasExceededStock || !formik.isValid || formik.isSubmitting
}
>
Submit
</Button>
</>
)}
{type === 'add' && (
<> <>
<Button <Button
type='reset' type='reset'
color='warning' color='neutral'
className='px-4' className='px-4'
onClick={(e) => { onClick={(e) => {
formik.handleReset(e); formik.handleReset(e);
@@ -2489,7 +2529,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
setTimeout(() => { setTimeout(() => {
router.push( router.push(
'/production/recording/grading/add?recording_id=1' `/production/recording/grading/add?recording_id=${initialValues?.id || ''}`
); );
}, 1000); }, 1000);
} }
@@ -2505,21 +2545,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
</> </>
)} )}
{type === 'detail' && isLayingCategory && (
<Button
type='button'
color='primary'
onClick={() => {
router.push(
`/production/recording/grading/add?recording_id=${initialValues?.id}`
);
}}
>
<Icon icon='material-symbols:egg' width={24} height={24} />
Lanjut ke Grading
</Button>
)}
</div> </div>
</div> </div>
{recordingFormErrorMessage && ( {recordingFormErrorMessage && (