feat(FE-170,175): implement multi-select approval and rejection for recordings in RecordingTable

This commit is contained in:
rstubryan
2025-11-05 10:01:07 +07:00
parent fa36c10c01
commit 4454eac8af
@@ -8,7 +8,7 @@ import { cn, formatDate } from '@/lib/helper';
import { useModal } from '@/components/Modal';
import Button from '@/components/Button';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { OptionType } from '@/components/input/SelectInput';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import SelectInput from '@/components/input/SelectInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { ROWS_OPTIONS } from '@/config/constant';
@@ -27,6 +27,9 @@ import toast from 'react-hot-toast';
import Badge from '@/components/Badge';
import CheckboxInput from '@/components/input/CheckboxInput';
import TextArea from '@/components/input/TextArea';
import { Area } from '@/types/api/master-data/area';
import { Location } from '@/types/api/master-data/location';
import { Kandang } from '@/types/api/master-data/kandang';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -65,28 +68,18 @@ const RowOptionsMenu = ({
onClick={approveClickHandler}
variant='ghost'
color='success'
className='justify-start text-sm text-success focus-visible:text-success-content hover:text-success-content'
className='justify-start text-sm'
>
<Icon
icon='mdi:check-circle-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
<Icon icon='material-symbols:check' width={16} height={16} />
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'
color='error'
className='justify-start text-sm'
>
<Icon
icon='mdi:close-circle-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
<Icon icon='material-symbols:close' width={16} height={16} />
Reject
</Button>
<Button
@@ -134,6 +127,10 @@ const RecordingTable = () => {
});
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
);
const [selectedRecording, setSelectedRecording] = useState<
Recording | undefined
>(undefined);
@@ -146,11 +143,7 @@ const RecordingTable = () => {
const approveModal = useModal();
const rejectModal = useModal();
// State for dropdown search
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
// State for selected values
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
@@ -169,55 +162,23 @@ const RecordingTable = () => {
);
// Fetch data for dropdowns
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
search: areaSelectInputValue,
limit: '100',
}).toString()}`;
const { data: areas, isLoading: isLoadingAreas } = useSWR(
areaUrl,
AreaApi.getAllFetcher
);
const {
setInputValue: setAreaSelectInputValue,
options: areaOptions,
isLoadingOptions: isLoadingAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
search: locationSelectInputValue,
area_id: selectedArea != null ? selectedArea.value.toString() : '',
limit: '100',
}).toString()}`;
const { data: locations, isLoading: isLoadingLocations } = useSWR(
locationUrl,
LocationApi.getAllFetcher
);
const {
setInputValue: setLocationSelectInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: kandangSelectInputValue,
location_id:
selectedLocation != null ? selectedLocation.value.toString() : '',
limit: '100',
}).toString()}`;
const { data: kandangs, isLoading: isLoadingKandang } = useSWR(
kandangUrl,
KandangApi.getAllFetcher
);
// Data to Options Mapping
const optionsArea = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const optionsLocation = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
const optionsKandang = isResponseSuccess(kandangs)
? kandangs?.data.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}))
: [];
const {
setInputValue: setKandangSelectInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandang,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -251,15 +212,18 @@ const RecordingTable = () => {
setIsApproveLoading(true);
const approveResponse = await RecordingApi.approve(
selectedRecording?.id as number,
selectedRowIds,
approvalNotes || 'Approved via Table'
);
if (isResponseSuccess(approveResponse)) {
toast.success('Recording berhasil disetujui!');
toast.success(
`Berhasil approve ${selectedRowIds.length} data recording!`
);
approveModal.closeModal();
refreshRecordings();
setApprovalNotes('');
setRowSelection({});
} else {
toast.error(
(approveResponse?.message as string) || 'Gagal menyetujui recording'
@@ -273,15 +237,16 @@ const RecordingTable = () => {
setIsRejectLoading(true);
const rejectResponse = await RecordingApi.reject(
selectedRecording?.id as number,
selectedRowIds,
approvalNotes || 'Rejected via Table'
);
if (isResponseSuccess(rejectResponse)) {
toast.success('Recording berhasil ditolak!');
toast.success(`Berhasil reject ${selectedRowIds.length} data recording!`);
rejectModal.closeModal();
refreshRecordings();
setApprovalNotes('');
setRowSelection({});
} else {
toast.error(
(rejectResponse?.message as string) || 'Gagal menolak recording'
@@ -294,8 +259,8 @@ const RecordingTable = () => {
return (
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row 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'>
<Button
href='recording/add'
variant='outline'
@@ -305,6 +270,38 @@ const RecordingTable = () => {
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
{selectedRowIds.length > 0 && (
<>
<Button
variant='outline'
color='success'
onClick={() => {
setApprovalNotes('');
approveModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='outline'
color='error'
onClick={() => {
setApprovalNotes('');
rejectModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</>
)}
</div>
<DebouncedTextInput
@@ -320,7 +317,7 @@ const RecordingTable = () => {
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
options={areaOptions}
isLoading={isLoadingAreas}
value={selectedArea}
onChange={(selected) => {
@@ -346,7 +343,7 @@ const RecordingTable = () => {
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
options={locationOptions}
isLoading={isLoadingLocations}
value={selectedLocation}
onChange={(selected) => {
@@ -371,7 +368,7 @@ const RecordingTable = () => {
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
options={kandangOptions}
isLoading={isLoadingKandang}
value={selectedKandang}
onChange={(selected) => {
@@ -521,13 +518,17 @@ const RecordingTable = () => {
};
const approveClickHandler = () => {
setSelectedRecording(props.row.original);
setRowSelection({
[String(props.row.original.id)]: true,
});
setApprovalNotes('');
approveModal.openModal();
};
const rejectClickHandler = () => {
setSelectedRecording(props.row.original);
setRowSelection({
[String(props.row.original.id)]: true,
});
setApprovalNotes('');
rejectModal.openModal();
};
@@ -571,6 +572,8 @@ const RecordingTable = () => {
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'mb-20':
@@ -605,13 +608,13 @@ const RecordingTable = () => {
<ConfirmationModal
ref={approveModal.ref}
type='success'
text={`Apakah anda yakin ingin menyetujui Recording ini?`}
text={`Apakah anda yakin ingin approve data recording ini (${selectedRowIds.length} data)?`}
secondaryButton={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
}}
primaryButton={{
text: 'Ya, Setujui',
text: 'Ya',
color: 'success',
isLoading: isApproveLoading,
onClick: approveHandler,
@@ -629,13 +632,13 @@ const RecordingTable = () => {
<ConfirmationModal
ref={rejectModal.ref}
type='error'
text={`Apakah anda yakin ingin menolak Recording ini?`}
text={`Apakah anda yakin ingin reject data recording ini (${selectedRowIds.length} data)?`}
secondaryButton={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
}}
primaryButton={{
text: 'Ya, Tolak',
text: 'Ya',
color: 'error',
isLoading: isRejectLoading,
onClick: rejectHandler,