mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-170,175): implement multi-select approval and rejection for recordings in RecordingTable
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user