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 =
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 (
<RowOptionsMenuWrapper type={type}>
<Button
@@ -82,6 +92,7 @@ const RowOptionsMenu = ({
Grading
</Button>
)}
{!isApproved && (
<Button
onClick={approveClickHandler}
variant='ghost'
@@ -91,6 +102,8 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:check' width={16} height={16} />
Approve
</Button>
)}
{!isApproved && (
<Button
onClick={rejectClickHandler}
variant='ghost'
@@ -100,6 +113,7 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:close' width={16} height={16} />
Reject
</Button>
)}
<Button
onClick={deleteClickHandler}
variant='ghost'
@@ -409,6 +423,14 @@ const RecordingTable = () => {
isLoadingOptions: isLoadingKandang,
} = 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(
(e: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', e.target.value);
@@ -440,14 +462,22 @@ const RecordingTable = () => {
const approveHandler = async () => {
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(
selectedRowIds,
eligibleRowIds,
approvalNotes
);
if (isResponseSuccess(approveResponse)) {
toast.success(
`Berhasil approve ${selectedRowIds.length} data recording!`
`Berhasil approve ${eligibleRowIds.length} data recording!`
);
approveModal.closeModal();
refreshRecordings();
@@ -465,13 +495,21 @@ const RecordingTable = () => {
const rejectHandler = async () => {
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(
selectedRowIds,
eligibleRowIds,
approvalNotes
);
if (isResponseSuccess(rejectResponse)) {
toast.success(`Berhasil reject ${selectedRowIds.length} data recording!`);
toast.success(`Berhasil reject ${eligibleRowIds.length} data recording!`);
rejectModal.closeModal();
refreshRecordings();
setApprovalNotes('');
@@ -485,6 +523,15 @@ const RecordingTable = () => {
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 (
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
@@ -647,17 +694,21 @@ const RecordingTable = () => {
/>
</div>
),
cell: ({ row }) => (
cell: ({ row }) => {
const isApproved = isRecordingApproved(row.original);
return (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
disabled={!row.getCanSelect() || isApproved}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
title={isApproved ? 'Recording sudah disetujui' : ''}
/>
</div>
),
);
},
},
{
header: '#',
@@ -893,7 +944,7 @@ const RecordingTable = () => {
<ConfirmationModal
ref={approveModal.ref}
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={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
@@ -917,7 +968,7 @@ const RecordingTable = () => {
<ConfirmationModal
ref={rejectModal.ref}
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={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
@@ -2405,9 +2405,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Action buttons */}
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{/* Left side - Detail & Edit actions */}
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
{deleteRecordingClickHandler && (
{type === 'detail' && deleteRecordingClickHandler && (
<Button
type='button'
color='error'
@@ -2423,7 +2424,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Delete
</Button>
)}
{type !== 'edit' && initialValues && (
{type === 'detail' && initialValues && (
<Button
type='button'
color='warning'
@@ -2442,11 +2443,50 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div>
)}
<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
type='reset'
color='warning'
color='neutral'
className='px-4'
onClick={(e) => {
formik.handleReset(e);
@@ -2489,7 +2529,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
);
setTimeout(() => {
router.push(
'/production/recording/grading/add?recording_id=1'
`/production/recording/grading/add?recording_id=${initialValues?.id || ''}`
);
}, 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>
{recordingFormErrorMessage && (