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