feat(FE-137): integrate advanced filtering options in RecordingTable with dropdowns for area, location, and kandang

This commit is contained in:
rstubryan
2025-10-24 13:53:20 +07:00
parent 537fc617ff
commit d14fa2ed2b
@@ -9,6 +9,7 @@ import { useModal } from '@/components/Modal';
import Button from '@/components/Button';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { OptionType } from '@/components/input/SelectInput';
import SelectInput from '@/components/input/SelectInput';
import { ROWS_OPTIONS } from '@/config/constant';
import CheckboxInput from '@/components/input/CheckboxInput';
import { TableToolbar } from '@/components/table/TableToolbar';
@@ -20,7 +21,11 @@ import { type CellContext } from '@tanstack/react-table';
import { type Recording } from '@/types/api/production/recording';
import { type BaseApiResponse } from '@/types/api/api-general';
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 { useTableFilter } from '@/services/hooks/useTableFilter';
import toast from 'react-hot-toast';
const RowOptionsMenu = ({
@@ -80,9 +85,31 @@ const RowOptionsMenu = ({
};
const RecordingTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
areaFilter: '',
locationFilter: '',
kandangFilter: '',
periodFilter: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
areaFilter: 'area_id',
locationFilter: 'location_id',
kandangFilter: 'kandang_id',
periodFilter: 'period',
},
});
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [selectedRecording, setSelectedRecording] = useState<Recording | undefined>(undefined);
@@ -94,21 +121,81 @@ const RecordingTable = () => {
const bulkApproveModal = useModal();
const bulkRejectModal = useModal();
// State for dropdown search
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(null);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(null);
const {
data: recordings,
isLoading,
mutate: refreshRecordings,
} = useSWR(
`${RecordingApi.basePath}?page=${page}&limit=${pageSize}`,
`${RecordingApi.basePath}${getTableFilterQueryString()}`,
RecordingApi.getAllFetcher
);
// 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 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 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 searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
updateFilter('search', e.target.value);
setPage(1);
},
[]
[updateFilter, setPage]
);
const pageSizeChangeHandler = useCallback(
@@ -117,22 +204,14 @@ const RecordingTable = () => {
setPageSize(newVal.value as number);
setPage(1);
},
[]
[setPageSize, setPage]
);
const paginatedData = useMemo(() => {
if (!recordings || recordings.status !== 'success') return [];
return recordings.data.filter(
(recording: Recording) => {
// For now, we don't have project_flock relation data in the API response
// So we'll filter by basic recording data
return recording.project_flock_kandang_id.toString().includes(search.toLowerCase()) ||
recording.record_date.includes(search.toLowerCase()) ||
recording.created_user.name.toLowerCase().includes(search.toLowerCase());
}
);
}, [recordings, search]);
return recordings.data;
}, [recordings]);
const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item));
@@ -210,16 +289,184 @@ const RecordingTable = () => {
label: 'Tambah Recording',
}}
search={{
value: search,
value: tableFilterState.search,
onChange: searchChangeHandler,
placeholder: 'Cari Recording',
}}
/>
<TableRowSizeSelector
value={pageSize}
value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/>
{/* Filter Dropdowns - Desktop */}
<div className='hidden sm:grid sm:grid-cols-4 gap-4 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedArea(selectedValue);
setSelectedLocation(null);
setSelectedKandang(null);
updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('locationFilter', '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setAreaSelectInputValue(value)}
isLoading={isLoadingAreas}
isClearable
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedLocation(selectedValue);
setSelectedKandang(null);
updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setLocationSelectInputValue(value)}
isLoading={isLoadingLocations}
isClearable
isDisabled={!selectedArea}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedKandang(selectedValue);
updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setKandangSelectInputValue(value)}
isLoading={isLoadingKandang}
isClearable
isDisabled={!selectedLocation}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
{/* Filter Dropdowns - Mobile */}
<div className='sm:hidden flex flex-col gap-3 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedArea(selectedValue);
setSelectedLocation(null);
setSelectedKandang(null);
updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('locationFilter', '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setAreaSelectInputValue(value)}
isLoading={isLoadingAreas}
isClearable
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedLocation(selectedValue);
setSelectedKandang(null);
updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : '');
updateFilter('kandangFilter', '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setLocationSelectInputValue(value)}
isLoading={isLoadingLocations}
isClearable
isDisabled={!selectedArea}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedKandang(selectedValue);
updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
onInputChange={(value) => setKandangSelectInputValue(value)}
isLoading={isLoadingKandang}
isClearable
isDisabled={!selectedLocation}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
</div>
{/* Bulk action buttons */}
@@ -315,7 +562,7 @@ const RecordingTable = () => {
},
{
header: '#',
cell: (props) => pageSize * (page - 1) + props.row.index + 1,
cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + 1,
},
{
header: 'ID',
@@ -427,8 +674,8 @@ const RecordingTable = () => {
},
},
]}
pageSize={pageSize}
page={recordings?.status === 'success' ? recordings.meta?.page : page}
pageSize={tableFilterState.pageSize}
page={recordings?.status === 'success' ? recordings.meta?.page : tableFilterState.page}
totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0}
onPageChange={setPage}
isLoading={isLoading}