feat(FE-170,175): add approval and rejection functionality with confirmation modals in RecordingTable

This commit is contained in:
rstubryan
2025-11-05 09:46:38 +07:00
parent 02cc4a759d
commit fa36c10c01
2 changed files with 159 additions and 1 deletions
@@ -30,6 +30,7 @@ interface ConfirmationModalProps {
modal?: string;
modalBox?: string;
};
children?: React.ReactNode;
}
const ConfirmationModal = ({
@@ -40,6 +41,7 @@ const ConfirmationModal = ({
primaryButton,
secondaryButton,
className,
children,
}: ConfirmationModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
@@ -90,6 +92,12 @@ const ConfirmationModal = ({
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
</p>
{children && (
<div className='w-full'>
{children}
</div>
)}
<div className='w-full flex flex-row gap-2'>
{secondaryButton && secondaryButton.text && (
<Button
@@ -21,20 +21,25 @@ import { RecordingApi } from '@/services/api/production';
import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data';
import { KandangApi } from '@/services/api/master-data';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import toast from 'react-hot-toast';
import Badge from '@/components/Badge';
import CheckboxInput from '@/components/input/CheckboxInput';
import TextArea from '@/components/input/TextArea';
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
approveClickHandler,
rejectClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Recording, unknown>;
deleteClickHandler: () => void;
approveClickHandler: () => void;
rejectClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
@@ -56,6 +61,34 @@ const RowOptionsMenu = ({
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={approveClickHandler}
variant='ghost'
color='success'
className='justify-start text-sm text-success focus-visible:text-success-content hover:text-success-content'
>
<Icon
icon='mdi:check-circle-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Approve
</Button>
<Button
onClick={rejectClickHandler}
variant='ghost'
color='warning'
className='justify-start text-sm text-warning focus-visible:text-warning-content hover:text-warning-content'
>
<Icon
icon='mdi:close-circle-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Reject
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
@@ -105,8 +138,13 @@ const RecordingTable = () => {
Recording | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [approvalNotes, setApprovalNotes] = useState('');
const singleDeleteModal = useModal();
const approveModal = useModal();
const rejectModal = useModal();
// State for dropdown search
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
@@ -209,6 +247,50 @@ const RecordingTable = () => {
setIsDeleteLoading(false);
};
const approveHandler = async () => {
setIsApproveLoading(true);
const approveResponse = await RecordingApi.approve(
selectedRecording?.id as number,
approvalNotes || 'Approved via Table'
);
if (isResponseSuccess(approveResponse)) {
toast.success('Recording berhasil disetujui!');
approveModal.closeModal();
refreshRecordings();
setApprovalNotes('');
} else {
toast.error(
(approveResponse?.message as string) || 'Gagal menyetujui recording'
);
}
setIsApproveLoading(false);
};
const rejectHandler = async () => {
setIsRejectLoading(true);
const rejectResponse = await RecordingApi.reject(
selectedRecording?.id as number,
approvalNotes || 'Rejected via Table'
);
if (isResponseSuccess(rejectResponse)) {
toast.success('Recording berhasil ditolak!');
rejectModal.closeModal();
refreshRecordings();
setApprovalNotes('');
} else {
toast.error(
(rejectResponse?.message as string) || 'Gagal menolak recording'
);
}
setIsRejectLoading(false);
};
return (
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
@@ -395,6 +477,10 @@ const RecordingTable = () => {
header: 'Status Approval',
cell: (props) => props.row.original.approval?.step_name || '-',
},
{
header: 'Catatan Approval',
cell: (props) => props.row.original.approval?.notes || '-',
},
{
header: 'Status Grading Telur',
cell: (props) => {
@@ -434,6 +520,18 @@ const RecordingTable = () => {
singleDeleteModal.openModal();
};
const approveClickHandler = () => {
setSelectedRecording(props.row.original);
setApprovalNotes('');
approveModal.openModal();
};
const rejectClickHandler = () => {
setSelectedRecording(props.row.original);
setApprovalNotes('');
rejectModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
@@ -442,6 +540,8 @@ const RecordingTable = () => {
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
/>
</RowDropdownOptions>
)}
@@ -452,6 +552,8 @@ const RecordingTable = () => {
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
/>
</RowCollapseOptions>
)}
@@ -499,6 +601,54 @@ const RecordingTable = () => {
onClick: singleDeleteHandler,
}}
/>
<ConfirmationModal
ref={approveModal.ref}
type='success'
text={`Apakah anda yakin ingin menyetujui Recording ini?`}
secondaryButton={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
}}
primaryButton={{
text: 'Ya, Setujui',
color: 'success',
isLoading: isApproveLoading,
onClick: approveHandler,
}}
>
<TextArea
name='approvalNotes'
placeholder='(Opsional) Tambahkan catatan untuk approval ini...'
value={approvalNotes}
onChange={(e) => setApprovalNotes(e.target.value)}
rows={3}
/>
</ConfirmationModal>
<ConfirmationModal
ref={rejectModal.ref}
type='error'
text={`Apakah anda yakin ingin menolak Recording ini?`}
secondaryButton={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
}}
primaryButton={{
text: 'Ya, Tolak',
color: 'error',
isLoading: isRejectLoading,
onClick: rejectHandler,
}}
>
<TextArea
name='rejectNotes'
placeholder='(Opsional) Tambahkan catatan untuk reject ini...'
value={approvalNotes}
onChange={(e) => setApprovalNotes(e.target.value)}
rows={3}
/>
</ConfirmationModal>
</div>
);
};