mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-170,175): enhance RecordingForm and RecordingTable with approval logic and UI improvements
This commit is contained in:
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user